diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,555 +1,709 @@
 
+5.28
+----
+
+This release improves Vty's support for multi-column Unicode characters
+and provides greater compatibility with a wider array of terminal
+emulators. The following sections summarize the relevant changes, but an
+overview of the new functionality is motivated and detailed in the new
+"Multi-Column Character Support" README section. For
+historical context, please also consider reading over
+[#175](https://github.com/jtdaugherty/vty/issues/175).
+
+API changes:
+ * New modules were added:
+   * `Graphics.Vty.UnicodeWidthTable.Types`
+   * `Graphics.Vty.UnicodeWidthTable.IO`
+   * `Graphics.Vty.UnicodeWidthTable.Query`
+   * `Graphics.Vty.UnicodeWidthTable.Install`
+ * The `Config` type got a new field, `allowCustomUnicodeWidthTables`,
+   that controls whether `mkVty` will attempt to load a Unicode width
+   table if specified in the configuration.
+
+Configuration file changes:
+ * A new syntax was added to support specifying Unicode width tables on
+   a per-`TERM` basis. The syntax is `widthMap <TERM> <PATH>`. See the
+   documentation for `Graphics.Vty.Config` for details. Since prior
+   versions of this library will silently ignore any configuration file
+   lines they cannot parse, this change to user configuration files is
+   at least non-breaking for older versions of Vty.
+
+Other changes:
+ * The `mkVty` function now automatically attempts to load a custom
+   Unicode width table if one is specified in the configuration,
+   provided `allowCustomUnicodeWidthTables` is not set to `Just False`.
+   See the documentation for `Graphics.Vty.mkVty` for details.
+ * Vty now includes a command line tool, `vty-build-width-table`, that
+   queries the terminal emulator to construct a custom Unicode width
+   table and optionally update the Vty configuration file to use it.
+   Programs that want to use that tool's functionality may also do so
+   via the API exposed in the various modules listed above.
+
 5.27
- - Added Graphics.Vty.Config.getTtyEraseChar to support querying the
-   kernel for the current terminal's settings to obtain the character
-   assigned by the "stty erase" command. That can then be added to the
-   Vty configuration's input map to map to KBS (backspace) if desired.
+----
 
+* Added `Graphics.Vty.Config.getTtyEraseChar` to support querying the
+  kernel for the current terminal's settings to obtain the character
+  assigned by the `stty erase` command. That can then be added to the
+  Vty configuration's input map to map to `KBS` (backspace) if desired.
+
 5.26
- - Resolved various import warnings (thanks @glguy)
- - Removed the MonadIO constraint from the Output type's fields and
-   removed MonadFail uses (PR #177, thanks @glguy)
- - Clarified documentation for ANSI colors (thanks Colby Jenn)
- - Graphics.Vty.Attributes no longer re-exports
-   Graphics.Vty.Attributes.Color
- - The Graphics.Vty.Attributes.Color module is now exposed (thanks Colby
-   Jenn)
- - Raised upper bound for microlens to 0.4.12 (thanks Artyom Kazak)
- - Changed from using System.Posix.Env.getEnv to
-   System.Environment.lookupEnv (thanks Jonathan Osser)
- - Added Graphics.Vty.Image functions for dealing with character width
-   computations on Text values instead of Strings:
-   - safeWctwidth
-   - safeWctlwidth
-   - wctwidth
-   - wctlwidth
+----
 
+* Resolved various import warnings (thanks @glguy)
+* Removed the `MonadIO` constraint from the Output type's fields and
+  removed `MonadFail` uses (PR #177, thanks @glguy)
+* Clarified documentation for ANSI colors (thanks Colby Jenn)
+* `Graphics.Vty.Attributes` no longer re-exports
+  `Graphics.Vty.Attributes.Color`
+* The `Graphics.Vty.Attributes.Color` module is now exposed (thanks
+  Colby Jenn)
+* Raised upper bound for `microlens` to 0.4.12 (thanks Artyom Kazak)
+* Changed from using `System.Posix.Env.getEnv` to
+  `System.Environment.lookupEnv` (thanks Jonathan Osser)
+* Added `Graphics.Vty.Image` functions for dealing with character width
+  computations on `Text` values instead of `Strings`:
+  * `safeWctwidth`
+  * `safeWctlwidth`
+  * `wctwidth`
+  * `wctlwidth`
+
 5.25.1
- - Avoided a conflict with a Microlens 0.4.10 operator and added an
-   upper bound on Microlens of 0.4.11.
+------
 
+* Avoided a conflict with a Microlens 0.4.10 operator and added an
+  upper bound on Microlens of 0.4.11.
+
 5.25
- - The Vty type got a new field, isShutdown, that returns whether the
-   Vty handle has had its 'shutdown' function called (thanks Ian
-   Jeffries)
- - Vty's shutdown function is now thread-safe.
+----
 
+* The Vty type got a new field, isShutdown, that returns whether the
+  Vty handle has had its 'shutdown' function called (thanks Ian
+  Jeffries)
+* Vty's shutdown function is now thread-safe.
+
 5.24.1
- - The "shutdown" method of Vty handles is now idempotent (#159)
+------
 
+* The "shutdown" method of Vty handles is now idempotent (#159)
+
 5.24
- - Add Generic and NFData instances for some types
- - Image: remove custom Show instance, add derived Show and Read instances
- - Updated Travis build settings (thanks Eric Mertens)
+----
 
+* Add Generic and NFData instances for some types
+* Image: remove custom Show instance, add derived Show and Read
+  instances
+* Updated Travis build settings (thanks Eric Mertens)
+
 5.23.1
-  - Fixed a bug where italics did not combine properly with other
-    display modes (#155, thanks Eric Mertens)
+------
 
+* Fixed a bug where italics did not combine properly with other display
+  modes (#155, thanks Eric Mertens)
+
 5.23
-  - Added support for italicized output when terminfo supports it. This
-    takes the form of a new Style, "italic". Note that most terminfo
-    descriptors do not report capabilities for italics, so support for
-    this will be very spotty.
-  - Updateed text/string function documentation to indicate that escapes
-    are not permitted in their inputs.
+----
 
+* Added support for italicized output when terminfo supports it. This
+  takes the form of a new Style, "italic". Note that most terminfo
+  descriptors do not report capabilities for italics, so support for
+  this will be very spotty.
+* Updateed text/string function documentation to indicate that escapes
+  are not permitted in their inputs.
+
 5.22
- - Added Graphics.Vty.Attributes.Color240.color240CodeToRGB function
-   (thanks Brent Carmer)
- - Added nextEventNonblocking function (field) to Vty type (#87)
+----
 
+* Added Graphics.Vty.Attributes.Color240.color240CodeToRGB function
+  (thanks Brent Carmer)
+* Added nextEventNonblocking function (field) to Vty type (#87)
+
 5.21
- - Picture and Background now provide Eq instances (thanks Jaro Reinders)
- - #145: vty builds with microlens 0.4.9 (thanks Daniel Wagner)
- - #142: note requirement of threaded RTS
+----
 
+* Picture and Background now provide Eq instances (thanks Jaro Reinders)
+* #145: vty builds with microlens 0.4.9 (thanks Daniel Wagner)
+* #142: note requirement of threaded RTS
+
 5.20
+----
+
 API changes:
- - Split up Monoid instances into Monoid and Semigroup for newer GHCs
-   (thanks Ryan Scott)
+* Split up Monoid instances into Monoid and Semigroup for newer GHCs
+  (thanks Ryan Scott)
 
 5.19.1
+------
+
 API changes:
- * Cursor now provides an Eq instance (thanks Jaro Reinders)
+* Cursor now provides an Eq instance (thanks Jaro Reinders)
 
 5.19
+----
+
 API changes:
- - URL hyperlinking (via 'withURL') is now optional and disabled by
-   default due to poor support on some common terminals. A new 'Mode'
-   constructor, 'Hyperlink', has been added to enable this feature. To
-   change the hyperlinking mode, use 'setMode' on the 'outputIface' of a
-   Vty handle.
+* URL hyperlinking (via 'withURL') is now optional and disabled by
+  default due to poor support on some common terminals. A new 'Mode'
+  constructor, 'Hyperlink', has been added to enable this feature. To
+  change the hyperlinking mode, use 'setMode' on the 'outputIface' of a
+  Vty handle.
 
 5.18.1
+------
+
 Bug fixes:
- - Reset the hyperlink state on line endings to avoid run-on hyperlinks
+* Reset the hyperlink state on line endings to avoid run-on hyperlinks
 
 5.18
+----
+
 API changes:
- - Added support for hyperlinking attributes (thanks Getty Ritter). This
-   change adds a new Attr field for containing the hyperlink to apply,
-   as per https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+* Added support for hyperlinking attributes (thanks Getty Ritter). This
+  change adds a new Attr field for containing the hyperlink to apply,
+  as per https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
 
 5.17.1
- - withStyle now ignores zero arguments, leaving attribute styles
-   untouched if the input style is the null style
+------
 
+* withStyle now ignores zero arguments, leaving attribute styles
+  untouched if the input style is the null style
+
 5.17
+----
+
 API changes:
- - Add support for terminal focus events. This change adds a new mode
-   usable with setMode, Focus, that requests that the terminal send
-   events on focus lose/gain. This change also adds two new Event
-   constructors, EvLostFocus and EvGainedFocus.
- - No longer enable UTF8 mouse event encoding. This encoding was not
-   working properly with Terminal.app, and using the other modes (SGR,
-   etc.) work.
- - Graphics.Vty.Attributes: escape backticks in Haddock comment (fixes
-   #131)
+* Add support for terminal focus events. This change adds a new mode
+  usable with setMode, Focus, that requests that the terminal send
+  events on focus lose/gain. This change also adds two new Event
+  constructors, EvLostFocus and EvGainedFocus.
+* No longer enable UTF8 mouse event encoding. This encoding was not
+  working properly with Terminal.app, and using the other modes (SGR,
+  etc.) work.
+* Graphics.Vty.Attributes: escape backticks in Haddock comment (fixes
+  #131)
 
 5.16
+----
+
 API changes:
-  - Added support for mouse wheel events while in mouse mode. The Button
-    type got two new constructors as a result: BScrollUp and BScrollDown.
-    Thanks to doublescale@tutanota.com for this contribution!
+* Added support for mouse wheel events while in mouse mode. The Button
+  type got two new constructors as a result: BScrollUp and BScrollDown.
+  Thanks to doublescale@tutanota.com for this contribution!
 
 Bug fixes:
-  - charFill now clamps negative arguments to zero (thanks Eric
-    Mertens!)
+* charFill now clamps negative arguments to zero (thanks Eric Mertens!)
 
 5.15.1
+------
+
 Package changes:
-  - Documentation files are now marked accordingly (thanks Michal
-    Suchánek)
+* Documentation files are now marked accordingly (thanks Michal
+  Suchánek)
 
 Bug fixes:
-  - translateX/Y: fix negative translations
+* translateX/Y: fix negative translations
 
 5.15
+----
+
 Package changes:
-  - Discontinued support for GHC versions prior to 7.10.1.
-  - Removed instructions and configuration for Stack builds since they
-    are no longer supported.
-  - Clarified README mention of (lack of) Windows support (contributors
-    wanted, though!)
-  - Removed dependency on data-default (see below).
+* Discontinued support for GHC versions prior to 7.10.1.
+* Removed instructions and configuration for Stack builds since they
+  are no longer supported.
+* Clarified README mention of (lack of) Windows support (contributors
+  wanted, though!)
+* Removed dependency on data-default (see below).
 
 API changes:
-  - Moved color definitions from Attributes to Color module.
-  - In lieu of data-default (Default) instances for Attr and Config, use
-    'defAttr' and the new 'defaultConfig' (or 'mempty') instead of 'def'.
-  - Graphics.Vty.Output no longer re-exports
-    Graphics.Vty.Output.Interface.
-  - Removed Graphics.Vty.Prelude module and moved DisplayRegion and its
-    accessors to Graphics.Vty.Image.
-  - Graphics.Vty.Image no longer re-exports Graphics.Vty.Attributes.
-  - Graphics.Vty.Picture no longer re-exports Graphics.Vty.Image.
+* Moved color definitions from Attributes to Color module.
+* In lieu of data-default (Default) instances for Attr and Config, use
+  'defAttr' and the new 'defaultConfig' (or 'mempty') instead of 'def'.
+* Graphics.Vty.Output no longer re-exports
+  Graphics.Vty.Output.Interface.
+* Removed Graphics.Vty.Prelude module and moved DisplayRegion and its
+  accessors to Graphics.Vty.Image.
+* Graphics.Vty.Image no longer re-exports Graphics.Vty.Attributes.
+* Graphics.Vty.Picture no longer re-exports Graphics.Vty.Image.
 
 5.14
-  - addMaybeClippedJoin: instead of raising an exception when the join
-    is totally clipped, just reduce the clip amount and continue
-  - addMaybeClipped: skip blit of joins when their primary dimension is
-    zero
-  - 'string' and related text functions no longer treat an empty string
-    as an empty image (thanks Chris Penner). This means that now it is
-    possible to use 'str ""' as a non-empty image with height 1.
+----
 
+* addMaybeClippedJoin: instead of raising an exception when the join
+  is totally clipped, just reduce the clip amount and continue
+* addMaybeClipped: skip blit of joins when their primary dimension is
+  zero
+* 'string' and related text functions no longer treat an empty string
+  as an empty image (thanks Chris Penner). This means that now it is
+  possible to use 'str ""' as a non-empty image with height 1.
+
 5.13
-  - Reverted changes in 5.12 due to disagreements between terminal
-    emulators and utf8proc; for more details, please see the ticket
-    discussion at
-    https://github.com/coreyoconnor/vty/issues/115
+----
 
+* Reverted changes in 5.12 due to disagreements between terminal
+  emulators and utf8proc; for more details, please see the ticket
+  discussion at
+  https://github.com/coreyoconnor/vty/issues/115
+
 5.12
-  - Replaced 'wcwidth' with a call to the utf8proc library's character
-    width function, which is much more up to date (by several Unicode
-    versions) and returns the right width for a much larger set of
-    characters.
-  - Added a bundled version of the utf8proc C library.
+----
 
+* Replaced 'wcwidth' with a call to the utf8proc library's character
+  width function, which is much more up to date (by several Unicode
+  versions) and returns the right width for a much larger set of
+  characters.
+* Added a bundled version of the utf8proc C library.
+
 5.11.3
-  - Fix mouse event offsets in mouse-up events
+------
 
+* Fix mouse event offsets in mouse-up events
+
 5.11.2
-  - Mouse events were modified so that the upper-left corner of the
-    window is (0,0) rather than (1,1).
+------
 
+* Mouse events were modified so that the upper-left corner of the window
+  is (0,0) rather than (1,1).
+
 5.11.1
-  - Add Generic instance for Image
-  - nextEvent: stop trying to refresh on a resize event (fixes segfault
-    on refresh with normal cursor positioning mode)
-  - Remove redundant clause from clipForCharWidth (thanks Eric Mertens)
-  - Update maintainer
+------
 
+* Add Generic instance for Image
+* nextEvent: stop trying to refresh on a resize event (fixes segfault
+  on refresh with normal cursor positioning mode)
+* Remove redundant clause from clipForCharWidth (thanks Eric Mertens)
+* Update maintainer
+
 5.11
-  - Vty now raises a VtyConfigurationError exception when the TERM
-    evironment variable is missing (thanks Eric Mertens)
-  - Graphics.Vty.Config got an explicit export list to avoid accidentally
-    exporting internal types (thanks Eric Mertens)
+----
 
+* Vty now raises a VtyConfigurationError exception when the TERM
+  evironment variable is missing (thanks Eric Mertens)
+* Graphics.Vty.Config got an explicit export list to avoid accidentally
+  exporting internal types (thanks Eric Mertens)
+
 5.10
-  - Add absolute cursor positioning mode AbsoluteCursor to Cursor. This
-    mode provides greater control over cursor positioning by bypassing
-    the logical positioning provided by default. Rather than positioning
-    the cursor by looking at the widths of characters involved, this
-    constructor lets you provide a physical row and column instead. This
-    is useful in more sophisticated programs. (thanks Eric Mertens)
-  - Added a new Generic-derived config parser (thanks Eric Mertens)
-  - Fixed the MShift case in the configuration file parser (thanks Eric
-    Mertens)
-  - Fixed wcwidth import and matched safeWcswidth to its documented
-    behavior. Previously vty_mk_wcwidth was being imported with
-    the wrong type causing the -1 return value to be mapped to the
-    wrong Int value. Additionally safeWcswidth was using the unsafe
-    character width function and only ensuring that the final result was
-    non-negative. (thanks Eric Mertens)
+----
 
+* Add absolute cursor positioning mode AbsoluteCursor to Cursor. This
+  mode provides greater control over cursor positioning by bypassing
+  the logical positioning provided by default. Rather than positioning
+  the cursor by looking at the widths of characters involved, this
+  constructor lets you provide a physical row and column instead. This
+  is useful in more sophisticated programs. (thanks Eric Mertens)
+* Added a new Generic-derived config parser (thanks Eric Mertens)
+* Fixed the MShift case in the configuration file parser (thanks Eric
+  Mertens)
+* Fixed wcwidth import and matched safeWcswidth to its documented
+  behavior. Previously vty_mk_wcwidth was being imported with the
+  wrong type causing the -1 return value to be mapped to the wrong Int
+  value. Additionally safeWcswidth was using the unsafe character width
+  function and only ensuring that the final result was non-negative.
+  (thanks Eric Mertens)
+
 5.9.1
-  - Vty now only emits UTF8 charset sequences in terminals without a
-    preexisting UTF8 declaration to avoid emitting garbage sequences
-    (fixes #89)
+-----
 
+* Vty now only emits UTF8 charset sequences in terminals without a
+  preexisting UTF8 declaration to avoid emitting garbage sequences
+  (fixes #89)
+
 5.9
-  - Added new Output methods supportsBell and ringTerminalBell to find
-    out whether the output device has an audio bell and to ring it (see
-    #102)
+---
 
+* Added new Output methods supportsBell and ringTerminalBell to find out
+  whether the output device has an audio bell and to ring it (see #102)
+
 5.8.1
-  - Fixed "refresh" to work as advertised (see #104)
+-----
 
+* Fixed "refresh" to work as advertised (see #104)
+
 5.8
-  - API change: EvPaste input event now provides paste data as a raw
-    ByteString rather than a String to allow the application to decode
-    how best to decode it
+---
 
+* API change: EvPaste input event now provides paste data as a raw
+  ByteString rather than a String to allow the application to decode how
+  best to decode it
+
 5.7.1
-  - ModeDemo: added an explicit Control.Applicative import for older
-    GHCs
+-----
 
+* ModeDemo: added an explicit Control.Applicative import for older GHCs
+
 5.7
-  - Mouse and paste modes are now off by default.
-  - The Config type got new fields: mouseMode and bracketedPasteMode.
-    These determine whether these modes are enabled initially (for
-    terminals that support them).
-  - Added a Mode type for modal terminal features (mouse events,
-    bracketed paste mode) that is used with new Output interface
-    functions:
-    * supportsMode :: Mode -> Bool tells whether the device supports a
-      mode
-    * setMode :: Mode -> Bool -> IO () turns a mode on or off
-    * getModeStatus :: Mode -> IO Bool tells you whether a mode is on or
-      off
-  - Added a new demo program, ModeDemo.hs, to demonstrate usage of modes
+---
 
+* Mouse and paste modes are now off by default.
+* The Config type got new fields: mouseMode and bracketedPasteMode.
+  These determine whether these modes are enabled initially (for
+  terminals that support them).
+* Added a Mode type for modal terminal features (mouse events,
+  bracketed paste mode) that is used with new Output interface
+  functions:
+  * supportsMode :: Mode -> Bool tells whether the device supports a
+    mode
+  * setMode :: Mode -> Bool -> IO () turns a mode on or off
+  * getModeStatus :: Mode -> IO Bool tells you whether a mode is on or
+    off
+* Added a new demo program, ModeDemo.hs, to demonstrate usage of modes
+
 5.6
-  - Added support for normal and extended mouse modes in Xterm-like
-    terminals via the MouseDown and MouseUp Event constructors
-  - Added support for bracketed paste mode in Xterm-like terminals via
-    the EvPaste event constructor
-  - Added derived Show instances for Event and Button (thanks Felix
-    Hirn)
-  - Now TERM values containing "screen" will automatically use the
-    XtermColor driver rather than just TerminfoBased
+---
 
+* Added support for normal and extended mouse modes in Xterm-like
+  terminals via the MouseDown and MouseUp Event constructors
+* Added support for bracketed paste mode in Xterm-like terminals via
+  the EvPaste event constructor
+* Added derived Show instances for Event and Button (thanks Felix
+  Hirn)
+* Now TERM values containing "screen" will automatically use the
+  XtermColor driver rather than just TerminfoBased
+
 5.5.0
-  - Replaced lens dependency with microlens, microlens-mtl, microlens-th
-    dependencies. Issue #90
-    - Thanks Jonathan Daugherty
-  - Cabal corrections.
-    - Thanks Lennart Spitzner
+-----
 
+* Replaced lens dependency with microlens, microlens-mtl, microlens-th
+  dependencies. Issue #90
+  * Thanks Jonathan Daugherty
+* Cabal corrections.
+  * Thanks Lennart Spitzner
+
 5.4.0
-  - Changed eventChannel of Graphics.Vty.Input from Chan to TChan.
-    This enables clients to query if there are no pending events. The
-    Graphics.Vty interface nextEvent is unchanged. Clients that use
-    eventChannel directly will require updating.
-    https://github.com/coreyoconnor/vty/issues/60
+-----
+
+* Changed eventChannel of Graphics.Vty.Input from Chan to
+  TChan. This enables clients to query if there are no pending
+  events. The Graphics.Vty interface nextEvent is unchanged.
+  Clients that use eventChannel directly will require updating.
+  https://github.com/coreyoconnor/vty/issues/60
+
 5.3.1
-  - Reverted cabal file to depend on Cabal >= 1.18 instead of 1.20 due
-    to possibly breaking this on reasonable GHC versions
+-----
 
+* Reverted cabal file to depend on Cabal >= 1.18 instead of 1.20 due to
+  possibly breaking this on reasonable GHC versions
+
 5.3
-  - Upgraded QuickCheck dependency to 2.7
-  - The standard IO Config (standardIOConfig) was overriding any
-    provided application config. In addition, the inputFd and outputFd
-    could not be changed if mkVty was used. Fixed.
-  - Correct handling of display attributes at end of line. The output
-    attributes are set to default at the end of content for the line and
-    at the start of a new line. Previously the current attribute would
-    extend to the next start of content. This was odd to reason about
-    and was the cause of https://github.com/coreyoconnor/vty/issues/76
-    IIRC Yi requires the old behavior to display the selection region
-    correctly.
-  - shutdown of the input thread is now performed using killThread and
-    synchronization on an MVar. For correct handling of the terminal
-    read vmin and vtime the read must be a blocking read on an OS
-    thread. This places a threadWaitRead, which will be interrupted by
-    the killThread, prior to the uninterruptable read. An alternative
-    would be to re-import the read foreign call as interruptable.
+---
 
+* Upgraded QuickCheck dependency to 2.7
+* The standard IO Config (standardIOConfig) was overriding any provided
+  application config. In addition, the inputFd and outputFd could not be
+  changed if mkVty was used. Fixed.
+* Correct handling of display attributes at end of line. The output
+  attributes are set to default at the end of content for the line and
+  at the start of a new line. Previously the current attribute would
+  extend to the next start of content. This was odd to reason about and
+  was the cause of https://github.com/coreyoconnor/vty/issues/76 IIRC Yi
+  requires the old behavior to display the selection region correctly.
+* shutdown of the input thread is now performed using killThread and
+  synchronization on an MVar. For correct handling of the terminal read
+  vmin and vtime the read must be a blocking read on an OS thread.
+  This places a threadWaitRead, which will be interrupted by the
+  killThread, prior to the uninterruptable read. An alternative would be
+  to re-import the read foreign call as interruptable.
+
 5.2.11
-  - deepseq bounds increased for tests.
-  - Clean up warnings when compiling on 7.10
-    - Thanks Eric Mertens
-  - Avoid discarding input bytes after multi-byte encoded codepoint
-    - Thanks Eric Mertens
+------
 
+* deepseq bounds increased for tests.
+* Clean up warnings when compiling on 7.10
+  * Thanks Eric Mertens
+* Avoid discarding input bytes after multi-byte encoded codepoint
+  * Thanks Eric Mertens
+
 5.2.10
-  - "str" now returns EmptyImage for empty strings to match behavior of
-    other string-like Image constructors (fixes #74)
-    - Thanks Jonathan Daugherty
+------
 
+* "str" now returns EmptyImage for empty strings to match behavior of
+  other string-like Image constructors (fixes #74)
+  * Thanks Jonathan Daugherty
+
 5.2.9
-  - dependency version bumps
-    - https://github.com/coreyoconnor/vty/pull/71
-    - https://github.com/coreyoconnor/vty/pull/70
-  - Correct/Simplify the example code
-    - Thanks glguy
-    - https://github.com/coreyoconnor/vty/pull/69
+-----
 
+* dependency version bumps
+  * https://github.com/coreyoconnor/vty/pull/71
+  * https://github.com/coreyoconnor/vty/pull/70
+* Correct/Simplify the example code
+  * Thanks glguy
+  * https://github.com/coreyoconnor/vty/pull/69
+
 5.2.8
-  - blaze-builder, lens, utf8-string version constraint bump
-      - Thanks glguy
-      - https://github.com/coreyoconnor/vty/pull/67
-  - Do not differentiate based on TERM_PROGRAM
-      - https://github.com/coreyoconnor/vty/issues/68
+-----
 
+* blaze-builder, lens, utf8-string version constraint bump
+    * Thanks glguy
+    * https://github.com/coreyoconnor/vty/pull/67
+* Do not differentiate based on TERM_PROGRAM
+    * https://github.com/coreyoconnor/vty/issues/68
+
 5.2.7
-  - lens and deepseq constraint bump + misc
-      - Thanks ethercrow
-      - https://github.com/coreyoconnor/vty/pull/66
+-----
 
+* lens and deepseq constraint bump + misc
+    * Thanks ethercrow
+    * https://github.com/coreyoconnor/vty/pull/66
+
 5.2.6
-  - lens constraint bump
-      - Thanks alexander-b!
-      - https://github.com/coreyoconnor/vty/pull/64
+-----
 
+* lens constraint bump
+    * Thanks alexander-b!
+    * https://github.com/coreyoconnor/vty/pull/64
+
 5.2.5
-  - lens and random version constraint bump.
-      - Thanks RyanGlScott!
-      - https://github.com/coreyoconnor/vty/pull/62
+-----
 
+* lens and random version constraint bump.
+    * Thanks RyanGlScott!
+    * https://github.com/coreyoconnor/vty/pull/62
+
 5.2.4
-  - removed -fpic from cc-options. No longer required.
-      - https://github.com/coreyoconnor/vty/issues/61
-      - https://ghc.haskell.org/trac/ghc/ticket/9657
-      - Thanks Fuuzetsu!
+-----
 
+* removed -fpic from cc-options. No longer required.
+    * https://github.com/coreyoconnor/vty/issues/61
+    * https://ghc.haskell.org/trac/ghc/ticket/9657
+    * Thanks Fuuzetsu!
+
 5.2.3
-  - evaluate/compile the input parsing table once instead of each
-    keystroke.
-      - https://github.com/coreyoconnor/vty/pull/59
-      - Thanks ethercrow!
+-----
 
+* evaluate/compile the input parsing table once instead of each
+  keystroke.
+    * https://github.com/coreyoconnor/vty/pull/59
+    * Thanks ethercrow!
+
 5.2.2
-  - When looking at input for an event, don't look too deep.
-      - https://github.com/coreyoconnor/vty/pull/57
-      - Thanks ethercrow!
+-----
 
+* When looking at input for an event, don't look too deep.
+    * https://github.com/coreyoconnor/vty/pull/57
+    * Thanks ethercrow!
+
 5.2.1
-  - Bump upper version bound for lens to 4.5. Thanks markus1189!
+-----
 
+* Bump upper version bound for lens to 4.5. Thanks markus1189!
+
 5.2.0
-  - Config structure now specifies file descriptor to use. The default
-    is stdInput and stdOutput file descriptors. Previously Vty used
-    stdInput for input and the follow code for output:
-      - hDuplicate stdout >>= handleToFd >>= (`hSetBuffering`
-        NoBuffering)
-      - the difference was required by Vty.Inline. Now, Vty.Inline uses
-        the Config structure options to acheive the same effect.
-  - removed: derivedVtime, derivedVmin, inputForCurrentTerminal,
-    inputForNameAndIO, outputForCurrentTerminal, outputForNameAndIO
-  - added: inputForConfig, outputForConfig
-  - updates to vty-rogue from jtdaugherty. Thanks!
-  - the oldest version of GHC tested to support vty is 7.6.2.
-  - the oldest version of GHC that vty compiles under is 7.4.2
+-----
 
+* Config structure now specifies file descriptor to use. The default
+  is stdInput and stdOutput file descriptors. Previously Vty used
+  stdInput for input and the follow code for output:
+    * hDuplicate stdout >>= handleToFd >>= (`hSetBuffering`
+      NoBuffering)
+    * the difference was required by Vty.Inline. Now, Vty.Inline uses
+      the Config structure options to acheive the same effect.
+* removed: derivedVtime, derivedVmin, inputForCurrentTerminal,
+  inputForNameAndIO, outputForCurrentTerminal, outputForNameAndIO
+* added: inputForConfig, outputForConfig
+* updates to vty-rogue from jtdaugherty. Thanks!
+* the oldest version of GHC tested to support vty is 7.6.2.
+* the oldest version of GHC that vty compiles under is 7.4.2
+
 5.1.4
-  - merged https://github.com/coreyoconnor/vty/pull/51 thanks trofi!
+-----
 
+* merged https://github.com/coreyoconnor/vty/pull/51 thanks trofi!
+
 5.1.1
-  - merged https://github.com/coreyoconnor/vty/pull/48 thanks sjmielke!
-  - jtdaugherty resolved a number of compiler warnings. Thanks!
+-----
 
+* merged https://github.com/coreyoconnor/vty/pull/48 thanks sjmielke!
+* jtdaugherty resolved a number of compiler warnings. Thanks!
+
 5.1.0
-  - vmin and vtime can be specified however the application requires.
-    See Graphics.Vty.Config.
-  - fixed the processing of input when vmin is set > 1.
+-----
 
+* vmin and vtime can be specified however the application requires.
+  See Graphics.Vty.Config.
+* fixed the processing of input when vmin is set > 1.
+
 5.0.0
-  - The naming convention now matches:
-    - http://www.haskell.org/haskellwiki/Programming_guidelines#Naming_Conventions
-  - all projects using vty for input must be compiled with -threaded.
-    Please notify vty author if this is not acceptable.
-  - mkVtyEscDelay has been removed. Use "mkVty def". Which initialized
-    vty with the default configuration.
-  - input handling changes
-    - KASCII is now KChar
-    - KPN5 is now KCenter
-    - tests exist.
-    - Applications can add to the input tables by setting inputMap of
-      the Config. See Graphics.Vty.Config
-    - Users can define input table extensions that will apply to all vty
-      applications. See Graphics.Vty.Config
-    - terminal timing is now handled by selecting an appropriate VTIME.
-      Previously this was implemented within Vty itself. This reduced
-      complexity in vty but provides a different meta key behavior and
-      implies a requirement on -threaded.
-    - The time vty will wait to verify an ESC byte means a single ESC
-      key is the singleEscPeriod of the Input Config structure.
-  - removed the typeclass based terminal and display context interface
-    in favor of a data structure of properties interface.
-  - renamed the Terminal interface to Output
-  - The default picture for an image now uses the "clear" background.
-    This background fills background spans with spaces or just ends the
-    line.
-    - Previously the background defaulted to the space character. This
-      causes issues copying text from a text editor. The text would end
-      up with extra spaces at the end of the line.
-  - Layer support
-    - Each layer is an image.
-    - The layers for a picture are a list of images.
-    - The first image is the top-most layer. The images are ordered from
-      top to bottom.
-    - The transparent areas for a layer are the backgroundFill areas.
-      backgroundFill is added to pad images when images of different
-      sizes are joined.
-    - If the background is clear there is no background layer.
-    - If there is a background character then the bottom layer is the
-      background layer.
-    - emptyPicture is a Picture with no layers and no cursor
-    - addToTop and addToBottom add a layer to the top and bottom of the
-      given Picture.
-  - compatibility improvements:
-    - terminfo based terminals with no cursor support are silently
-      accepted. The cursor visibility changes in the Picture will have
-      no effect.
-    - alternate (setf/setb) color maps supported. Though colors beyond
-      the first 8 are just a guess.
-    - added "rgbColor" for easy support of RGB specified colors.
-    - Both applications and users can add to the mapping used to
-      translate from input bytes to events.
-  - Additional information about input and output process can be
-    appended to a debug log
-    - Set environment variable VTY_DEBUG_LOG to path of debug log
-    - Or use "debugLog <path>" config directive
-    - Or set 'debugLog' property of the Config provided to mkVty.
-  - examples moved to vty-examples package. See test directory for cabal
-    file.
-    - vty-interactive-terminal-test
-      - interactive test. Useful for building a bug report for vty's
-        author.
-      - test/interactive_terminal_test.hs
-    - vty-event-echo
-      - view a input event log for vty. Example of interacting with
-        user.
-      - test/EventEcho.hs
-    - vty-rogue
-      - The start of a rogue-like game. Example of layers and image
-        build operations.
-      - test/Rogue.hs
-    - vty-benchmark
-      - benchmarks vty. A series of tests that push random pictures to
-        the terminal. The random pictures are generated using
-        QuickCheck. The same generators used in the automated tests.
-      - test/benchmark.hs
+-----
 
+* The naming convention now matches:
+  * http://www.haskell.org/haskellwiki/Programming_guidelines#Naming_Conventions
+* all projects using vty for input must be compiled with -threaded.
+  Please notify vty author if this is not acceptable.
+* mkVtyEscDelay has been removed. Use "mkVty def". Which initialized
+  vty with the default configuration.
+* input handling changes
+  * KASCII is now KChar
+  * KPN5 is now KCenter
+  * tests exist.
+  * Applications can add to the input tables by setting inputMap of
+    the Config. See Graphics.Vty.Config
+  * Users can define input table extensions that will apply to all vty
+    applications. See Graphics.Vty.Config
+  * terminal timing is now handled by selecting an appropriate VTIME.
+    Previously this was implemented within Vty itself. This reduced
+    complexity in vty but provides a different meta key behavior and
+    implies a requirement on -threaded.
+  * The time vty will wait to verify an ESC byte means a single ESC
+    key is the singleEscPeriod of the Input Config structure.
+* removed the typeclass based terminal and display context interface
+  in favor of a data structure of properties interface.
+* renamed the Terminal interface to Output
+* The default picture for an image now uses the "clear" background.
+  This background fills background spans with spaces or just ends the
+  line.
+  * Previously the background defaulted to the space character. This
+    causes issues copying text from a text editor. The text would end
+    up with extra spaces at the end of the line.
+* Layer support
+  * Each layer is an image.
+  * The layers for a picture are a list of images.
+  * The first image is the top-most layer. The images are ordered from
+    top to bottom.
+  * The transparent areas for a layer are the backgroundFill areas.
+    backgroundFill is added to pad images when images of different
+    sizes are joined.
+  * If the background is clear there is no background layer.
+  * If there is a background character then the bottom layer is the
+    background layer.
+  * emptyPicture is a Picture with no layers and no cursor
+  * addToTop and addToBottom add a layer to the top and bottom of the
+    given Picture.
+* compatibility improvements:
+  * terminfo based terminals with no cursor support are silently
+    accepted. The cursor visibility changes in the Picture will have
+    no effect.
+  * alternate (setf/setb) color maps supported. Though colors beyond
+    the first 8 are just a guess.
+  * added "rgbColor" for easy support of RGB specified colors.
+  * Both applications and users can add to the mapping used to
+    translate from input bytes to events.
+* Additional information about input and output process can be
+  appended to a debug log
+  * Set environment variable VTY_DEBUG_LOG to path of debug log
+  * Or use "debugLog <path>" config directive
+  * Or set 'debugLog' property of the Config provided to mkVty.
+* examples moved to vty-examples package. See test directory for cabal
+  file.
+  * vty-interactive-terminal-test
+    * interactive test. Useful for building a bug report for vty's
+      author.
+    * test/interactive_terminal_test.hs
+  * vty-event-echo
+    * view a input event log for vty. Example of interacting with
+      user.
+    * test/EventEcho.hs
+  * vty-rogue
+    * The start of a rogue-like game. Example of layers and image
+      build operations.
+    * test/Rogue.hs
+  * vty-benchmark
+    * benchmarks vty. A series of tests that push random pictures to
+      the terminal. The random pictures are generated using
+      QuickCheck. The same generators used in the automated tests.
+    * test/benchmark.hs
+
 4.7.0.0
-  - API changes:
-    - Added Graphics.Vty.Image.crop: Ensure an image is no larger
-      than the specified size.
-    - Added Graphics.Vty.Image.pad: Ensure an image is no smaller
-      than the specified size.
-    - Added Graphics.Vty.Image.translate: Offset an image.
-  - Thanks Ben Boeckel <MathStuf@gmail.com> for these features.
+-------
 
-4.3.0.0
+API changes:
+* Added Graphics.Vty.Image.crop: Ensure an image is no larger
+  than the specified size.
+* Added Graphics.Vty.Image.pad: Ensure an image is no smaller
+  than the specified size.
+* Added Graphics.Vty.Image.translate: Offset an image.
+* Thanks Ben Boeckel <MathStuf@gmail.com> for these features.
 
 4.2.1.0
-  - API changes:
-    - Attr record accessor fore_color changed to attr_fore_color
-    - Attr record accessor back_color changed to attr_back_color
-    - Attr record accessor style changed to attr_style
-    - Added an "inline" display attribute changing DSL:
-      - put_attr_change applies a display attribute change
-        immediately to a terminal
-      - For instance, can be used to change the display attrbiutes
-        of text output via putStrLn and putStr. EX:
-        "put_attr_change $ back_color red" will set the background
-        color to red.
-      - Changes do not apply to a Picture output via output_picture.
-      - See Graphics.Vty.Inline
-    - Moved all IO actions into any monad an instance of MonadIO
+-------
 
+API changes:
+* Attr record accessor fore_color changed to attr_fore_color
+* Attr record accessor back_color changed to attr_back_color
+* Attr record accessor style changed to attr_style
+* Added an "inline" display attribute changing DSL:
+  * put_attr_change applies a display attribute change
+    immediately to a terminal
+  * For instance, can be used to change the display attrbiutes
+    of text output via putStrLn and putStr. EX:
+    "put_attr_change $ back_color red" will set the background
+    color to red.
+  * Changes do not apply to a Picture output via output_picture.
+  * See Graphics.Vty.Inline
+* Moved all IO actions into any monad an instance of MonadIO
+
 4.0.0.1
-  - binding for mk_wcswidth was incorrect. Most platforms just
-    magically worked due to coincidence.
+-------
 
+* binding for mk_wcswidth was incorrect. Most platforms just
+  magically worked due to coincidence.
+
 4.0.0
-  - API changes:
-    - "getSize" has been removed. Use "terminal vty >>= display_bounds"
-      where "vty" is an instance of the Vty data structure.
-    - added a "terminal" field to the Vty data structure. Accesses the
-      TerminalHandle associated with the Vty instance.
-    - Graphics.Vty.Types has undergone a number of changes. Summary:
-      - Partitioned into Graphics.Vty.Attributes for display attributes.
-        Graphics.Vty.Image for image combinators. Graphics.Vty.Picture
-        for final picture construction.
-    - Graphics.Vty.Attributes:
-      - "setFG" and "setBG" are now "with_fore_color" and
-        "with_back_color"
-      - All other "set.." equations similarly replaced.
-      - "attr" is now "def_attr", short for "default display attributes"
-        Also added a "current_attr" for "currently applied display
-        attributes"
-    - Graphics.Vty.Image:
-      - "horzcat" is now "horiz_cat"
-      - "vertcat" is now "vert_cat"
-      - "renderBS" is now "utf8_bytestring"
-      - "renderChar" is now "char"
-      - "renderFill" is now "char_fill"
-      - added a "utf8_string" and "string" (AKA "iso_10464_string") for
-        UTF-8 encoded Strings and ISO-10464 encoded Strings. String
-        literals in GHC have an ISO-10464 runtime representation.
-    - Graphics.Vty.Picture:
-      - exports Graphics.Vty.Image
-      - "pic" is now "pic_for_image"
-      - added API for setting background fill pattern.
-  - Completely rewritten output backend.
-    - Efficient, scanline style output span generator. Has not been
-      fully optimized, but good enough.
-    - The details required to display the desired picture on a
-      terminal are well encapsulated.
-    - Terminfo based display terminal implementation. With specialized
-      derivitives for xterm, Terminal.app, and iTerm.app.
-        - Attempts to robustly handle even terminals that don't
-          support all display attributes.
-        - I've tested the following terminals with success: iTerm.app,
-          Terminal.app, xterm, rxvt, mlterm, Eterm, gnome-terminal,
-          konsole, screen, linux vty. Hopefully you will be as
-          successfull.
-    - Improved unicode support. Double wide characters will display as
-      expected.
-  - 256 color support. See Graphics.Vty.Attributes.Color240. The actual
-    output color is adjusted according to the number of colors the
-    terminal supports.
-  - The Graphics.Vty.Image combinators no longer require matching
-    dimensions to arguments. Unspecified areas are filled in with a
-    user-customizable background pattern. See Graphics.Vty.Picture.
-  - output images are always cropped to display size.
-  - Significant code coverage by QuickCheck tests. An interactive test
-    for those final properties that couldn't be automatically verified.
+-----
 
+API changes:
+* "getSize" has been removed. Use "terminal vty >>= display_bounds"
+  where "vty" is an instance of the Vty data structure.
+* added a "terminal" field to the Vty data structure. Accesses the
+  TerminalHandle associated with the Vty instance.
+* Graphics.Vty.Types has undergone a number of changes. Summary:
+  * Partitioned into Graphics.Vty.Attributes for display attributes.
+    Graphics.Vty.Image for image combinators. Graphics.Vty.Picture
+    for final picture construction.
+* Graphics.Vty.Attributes:
+  * "setFG" and "setBG" are now "with_fore_color" and
+    "with_back_color"
+  * All other "set.." equations similarly replaced.
+  * "attr" is now "def_attr", short for "default display attributes"
+    Also added a "current_attr" for "currently applied display
+    attributes"
+* Graphics.Vty.Image:
+  * "horzcat" is now "horiz_cat"
+  * "vertcat" is now "vert_cat"
+  * "renderBS" is now "utf8_bytestring"
+  * "renderChar" is now "char"
+  * "renderFill" is now "char_fill"
+  * added a "utf8_string" and "string" (AKA "iso_10464_string") for
+    UTF-8 encoded Strings and ISO-10464 encoded Strings. String
+    literals in GHC have an ISO-10464 runtime representation.
+* Graphics.Vty.Picture:
+  * exports Graphics.Vty.Image
+  * "pic" is now "pic_for_image"
+  * added API for setting background fill pattern.
+* Completely rewritten output backend.
+  * Efficient, scanline style output span generator. Has not been
+    fully optimized, but good enough.
+  * The details required to display the desired picture on a
+    terminal are well encapsulated.
+  * Terminfo based display terminal implementation. With specialized
+    derivitives for xterm, Terminal.app, and iTerm.app.
+      * Attempts to robustly handle even terminals that don't
+        support all display attributes.
+      * I've tested the following terminals with success: iTerm.app,
+        Terminal.app, xterm, rxvt, mlterm, Eterm, gnome-terminal,
+        konsole, screen, linux vty. Hopefully you will be as
+        successfull.
+  * Improved unicode support. Double wide characters will display as
+    expected.
+* 256 color support. See Graphics.Vty.Attributes.Color240. The actual
+  output color is adjusted according to the number of colors the
+  terminal supports.
+* The Graphics.Vty.Image combinators no longer require matching
+  dimensions to arguments. Unspecified areas are filled in with a
+  user-customizable background pattern. See Graphics.Vty.Picture.
+* output images are always cropped to display size.
+* Significant code coverage by QuickCheck tests. An interactive test
+  for those final properties that couldn't be automatically verified.
+
 Issues resolved:
-  - "gnome terminal displays non-basic attributes as strikethrough"
-    - http://trac.haskell.org/vty/ticket/14
-  - "Multi-byte characters are not displayed correctly on update"
-    - http://trac.haskell.org/vty/ticket/10
-  - "Redraw does not handle rendering a line that extends beyond screen
-    width characters"
-    - http://trac.haskell.org/vty/ticket/13
-  - "The <|> and <-> combinators should be more forgiving of mismatched
-    dimensions"
-    - http://trac.haskell.org/vty/ticket/9
-  - "256-color support"
-    - http://trac.haskell.org/vty/ticket/19
+* "gnome terminal displays non-basic attributes as strikethrough"
+  * http://trac.haskell.org/vty/ticket/14
+* "Multi-byte characters are not displayed correctly on update"
+  * http://trac.haskell.org/vty/ticket/10
+* "Redraw does not handle rendering a line that extends beyond screen
+  width characters"
+  * http://trac.haskell.org/vty/ticket/13
+* "The <|> and <-> combinators should be more forgiving of mismatched
+  dimensions"
+  * http://trac.haskell.org/vty/ticket/9
+* "256-color support"
+  * http://trac.haskell.org/vty/ticket/19
diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -18,24 +18,21 @@
 
 # Features
 
-* Supports a large number of terminals. vt100, ansi, hurd, linux,
-  screen, etc., or anything with a sufficient terminfo entry.
-
-* Automatic handling of window resizes.
+* Supports a large number of terminals, i.e., vt100, ansi, hurd, linux,
+  `screen`, etc., or anything with a sufficient terminfo entry.
 
-* Unicode output on terminals with UTF-8 support.
+* Automatically handles window resizes.
 
-* Handles multi-column glyphs. (Requires user to properly configure
-  terminal.)
+* Supports Unicode output on terminals with UTF-8 support.
 
-* Efficient output. Output buffering and terminal state changes are
-  minimized.
+* Provides an efficient output algorithm. Output buffering and terminal
+  state changes are minimized.
 
 * Minimizes repaint area, which virtually eliminates the flicker
   problems that plague ncurses programs.
 
-* A pure, compositional interface for efficiently constructing display
-  images.
+* Provides a pure, compositional interface for efficiently constructing
+  display images.
 
 * Automatically decodes keyboard keys into (key,[modifier]) tuples.
 
@@ -44,7 +41,7 @@
 * Supports a keypress timeout after for lone ESC. The timeout is
   customizable.
 
-* The interface is designed for easy extension.
+* Provides extensible input and output interfaces.
 
 * Supports ANSI graphics modes (SGR as defined in `console_codes(4)`)
   with a type-safe interface and graceful fallback for terminals
@@ -60,6 +57,12 @@
 * Supports bracketed paste mode as described at
   http://cirw.in/blog/bracketed-paste
 
+* Supports multi-column Unicode characters such as emoji characters. In
+  cases where Vty and your terminal emulator disagree on character
+  widths, Vty provides a tool `vty-build-width-table` and library
+  functionality to build a width table that will work for your terminal
+  and load it on application startup.
+
 # Development Notes
 
 Vty uses threads internally, so programs made with Vty need to be
@@ -69,64 +72,111 @@
 
 ## Posix Terminals
 
-Uses terminfo to determine terminal protocol. With some special rules to
-handle some omissions from terminfo.
+For the most part, Vty uses `terminfo` to determine terminal protocol
+with some special rules to handle some omissions from `terminfo`.
 
 ## Windows
 
-Unsupported (but contributions and testing here are very welcome!)
+Windows is not supported.
 
-# Contributing
+# Multi-Column Character Support
 
-If you decide to contribute, that's great! Here are some guidelines you
-should consider to make submitting patches easier for all concerned:
+Vty supports rendering of multi-column characters such as two-column
+Asian characters and Emoji characters. This section details how to
+take advantage of this feature, since its behavior will depend on the
+terminal emulator in use.
 
- - If you want to take on big things, talk to me first; let's have a
-   design/vision discussion before you start coding. Create a GitHub
-   issue and we can use that as the place to hash things out.
- - If you make changes, make them consistent with the syntactic
-   conventions already used in the codebase.
- - Please provide Haddock documentation for any changes you make.
+Terminal emulators support Unicode to varying degrees, and each terminal
+emulator relies on a table of column widths for each supported Unicode
+character. Vty also needs to rely on such a table to compute the width
+of Vty images to do image layout. Since those tables can disagree if
+Vty and the terminal emulator support different versions of Unicode,
+and since different terminal emulators will support different versions
+of Unicode, it's likely that for some wide characters, Vty applications
+will exhibit rendering problems. Those rendering problems arise from Vty
+and the terminal emulator coming to different conclusions about how wide
+some characters are.
 
-# Development Notes
+To address this, Vty supports loading custom character width tables
+that are based on the terminal's behavior in order to eliminate these
+disagreements. By default, though, Vty will use its built-in Unicode
+character width table. Since the built-in table is likely to eventually
+disagree with your terminal, Vty provides an API and a command-line tool
+to generate and install custom tables.
 
-## Under NixOS
+Custom Unicode width tables based on your terminal emulator can be
+built by running Vty's built-in tool, `vty-build-width-table`. The tool
+works by querying the current terminal emulator to obtain its width
+measurements for the entire supported Unicode range. The
+results are then saved to a disk file. These custom tables
+can also be generated programmatically by using the API in
+`Graphics.Vty.UnicodeWidthTable.Query`.
 
-### Using cabal
+Saved width tables can then be loaded in one of two ways:
 
-After installing `ncurses` to the user environment:
+* Via the library API in `Graphics.Vty.UnicodeWidthTable.IO`
+* By adding a `widthMap` directive to your Vty configuration file and
+  then invoking `mkVty` to initialize Vty
 
-~~~
-LIBRARY_PATH=$HOME/.nix-profile/lib/ cabal configure --enable-tests --extra-lib-dirs=$HOME/.nix-profile/lib
-LIBRARY_PATH=$HOME/.nix-profile/lib/ cabal build
-LIBRARY_PATH=$HOME/.nix-profile/lib/ cabal test
-~~~
+The Vty configuration file supports the `widthMap` directive to allow
+users to specify which custom width table should be loaded for a given
+terminal type. This is done by specifying, e.g.,
 
-## Code Coverage
+```
+widthMap "xterm" "/path/to/map.dat"
+```
 
-As of last testing, profiling causes issues with coverage when enabled.
-To evaluate coverage, configure as follows:
+where the first argument is the value that `TERM` must have in order for
+the table to be loaded, and the second argument is the path to the table
+file itself as generated by the two alternatives listed above. If the
+Vty configuration file contains multiple matching `widthMap` directives
+for the current value of `TERM`, the last one listed in the file is
+used.
 
-~~~
-rm -rf dist ; cabal configure --enable-tests --enable-library-coverage \
-  --disable-library-profiling \
-  --disable-executable-profiling
-~~~
+The tables declared in the configuration file are only ever
+automatically loaded when applications set up Vty by calling
+`Graphics.Vty.mkVty`.
 
-## Profiling
+Before a custom table has been loaded, calls to the library's character
+width functions (e.g. `wcwidth`) will use the default built-in table.
+Once a custom table has been loaded, the functions will use the new
+custom table. Only one custom table load can be performed in a Vty
+program. Once a custom table has been loaded, it cannot be replaced or
+removed.
 
-~~~
-rm -rf dist ; cabal configure --enable-tests --disable-library-coverage \
-  --enable-library-profiling \
-  --enable-executable-profiling
-~~~
+Without using a custom width table, users of Vty-based applications
+are likely to eventually experience rendering problems with with wide
+characters. We recommend that developers of Vty-based applications either:
 
+* Provide the `vty-build-width-table` tool and documentation for running
+  it and updating the Vty configuration, or
+* Have the application invoke the Vty library's table-building
+  functionality and load the table at startup without using the Vty
+  configuration.
+
+The best option will depend on a number of factors: the user audience,
+the amount of risk posed by wide character rendering, the terminal
+emulators in use, etc.
+
+# Contributing
+
+If you decide to contribute, that's great! Here are some guidelines you
+should consider to make submitting patches easier for all concerned:
+
+ - If you want to take on big things, talk to me first; let's have a
+   design/vision discussion before you start coding. Create a GitHub
+   issue and we can use that as the place to hash things out.
+ - If you make changes, make them consistent with the syntactic
+   conventions already used in the codebase.
+ - Please provide Haddock documentation for any changes you make.
+
 # Known Issues
 
-* Terminals have numerous quirks and bugs. Vty picks what works best for
-  the author in ambiguous or buggy situations.
+* Terminals have numerous quirks and bugs, so mileage may vary. Please
+  report issues as you encounter them and provide details on your
+  terminal emulator, operating system, etc.
 
-* Signal handling of STOP, TERM and INT are nonexistent.
+* STOP, TERM and INT signals are not handled.
 
 * The character encoding of the terminal is assumed to be UTF-8 if
   unicode is used.
@@ -136,10 +186,10 @@
   (shifted F10 etc). See `Config` for customizing vty's behavior for a
   particular terminal.
 
-* Uses the `TIOCGWINSZ` ioctl to find the current window size, which
+* Vty uses the `TIOCGWINSZ` ioctl to find the current window size, which
   appears to be limited to Linux and BSD.
 
-# Sources
+# Further Reading
 
 Good sources of documentation for terminal programming are:
 
diff --git a/cbits/mk_wcwidth.c b/cbits/mk_wcwidth.c
--- a/cbits/mk_wcwidth.c
+++ b/cbits/mk_wcwidth.c
@@ -60,7 +60,29 @@
  */
 
 #include <HsFFI.h>
+#include <stdlib.h>
+#include <string.h>
 
+// The maximum size for a custom character width table is the total
+// number of possible characters as dictated by the compiler.
+#define MAX_CUSTOM_TABLE_SIZE (HS_CHAR_MAX + 1)
+
+// The pointer to the start of the custom character width table, if
+// any. If this is NULL or this is set but the ready flag is false, the
+// built-in tree search logic is used.
+static uint8_t* custom_table = NULL;
+
+// The size of the custom table, in entries. This should only be set
+// if custom_table is not NULL. Its value should be the size of the
+// custom_table array.
+static uint32_t custom_table_size = 0;
+
+// A flag indicating whether the custom table is ready for
+// use. This should only be set once the table has been
+// allocated with vty_init_custom_table and initialized with
+// vty_set_custom_table_range.
+static uint8_t custom_table_ready = 0;
+
 struct interval {
   int first;
   int last;
@@ -119,7 +141,7 @@
  * in ISO 10646.
  */
 
-HsInt vty_mk_wcwidth(HsChar ucs)
+static HsInt builtin_wcwidth(HsChar ucs)
 {
   /* sorted list of non-overlapping intervals of non-spacing characters */
   /* generated by "uniset +cat=Me +cat=Mn +cat=Cf -00AD +1160-11FF +200B c" */
@@ -182,7 +204,7 @@
 
   /* binary search in table of non-spacing characters */
   if (vty_bisearch(ucs, combining,
-	       sizeof(combining) / sizeof(struct interval) - 1))
+              sizeof(combining) / sizeof(struct interval) - 1))
     return 0;
 
   /* if we arrive here, ucs is not a combining or C0/C1 control character */
@@ -201,4 +223,116 @@
       (ucs >= 0xffe0 && ucs <= 0xffe6) ||
       (ucs >= 0x20000 && ucs <= 0x2fffd) ||
       (ucs >= 0x30000 && ucs <= 0x3fffd)));
+}
+
+// Return the width, in terminal cells, of the specified character.
+//
+// If the global custom width table is present, that table will be
+// consulted for the character's width. If the character is not in
+// the table, zero will be returned. If the custom width table is not
+// present, the built-in width table will be used.
+HsInt vty_mk_wcwidth(HsChar ch)
+{
+    if (custom_table_ready) {
+        if ((ch >= 0) && (ch < custom_table_size)) {
+            return custom_table[ch];
+        } else {
+            return -1;
+        }
+    } else {
+        return builtin_wcwidth(ch);
+    }
+}
+
+// Initialize a custom character width table.
+//
+// This allocates a new character width table of the specified size
+// (in characters). If a custom table has already been allocated, this
+// returns 1. Otherwise it allocates a new table, initializes all of its
+// entries to 1, and returns zero.
+//
+// Note that this does *not* mark the table as ready for use. Until the
+// table is marked ready, it will not be used by vty_mk_wcwidth. To mark
+// the table as ready, call vty_activate_custom_table() after the table
+// has been set up with calls to vty_set_custom_table_range.
+int vty_init_custom_table(int size)
+{
+    if (custom_table == NULL) {
+        if (size > 0 && size <= MAX_CUSTOM_TABLE_SIZE) {
+            custom_table_ready = 0;
+            custom_table = malloc(size);
+            memset(custom_table, 1, size);
+            custom_table_size = size;
+            return 0;
+        } else {
+            return 1;
+        }
+    } else {
+        return 1;
+    }
+}
+
+// Set the specified character range in the custom width table to the
+// specified width.
+//
+// This function sets 'width' as the character width for all entries
+// in the custom character table starting at the 'start' entry and
+// including all entries up to and including 'start + size - 1'.
+//
+// If this succeeds, it returns zero. If it fails, it returns 1. It
+// fails if the table is not allocated, marked as ready (i.e. it is in
+// use and has already been populated), or if the start or size values
+// are not in bounds for the table.
+int vty_set_custom_table_range(uint32_t start, uint32_t size, uint8_t width)
+{
+    if ((custom_table == NULL) ||
+        (size >= custom_table_size) ||
+        (start >= custom_table_size) ||
+        ((start + 1) >= (custom_table_size - size)) ||
+        custom_table_ready) {
+        return 1;
+    } else {
+        memset(custom_table + start, width, size);
+        return 0;
+    }
+}
+
+// Mark the allocated custom character width table as ready for use.
+//
+// After this call, further calls to vty_set_custom_table_range will
+// fail.
+//
+// This function returns 0 if it succeeds. If it fails, it returns 1.
+// It fails if the custom table is already ready or if it has not been
+// allocated.
+int vty_activate_custom_table()
+{
+    if (custom_table_ready || (custom_table == NULL)) {
+        return 1;
+    } else {
+        custom_table_ready = 1;
+        return 0;
+    }
+}
+
+// Returns whether a custom character width table has been marked ready.
+int vty_custom_table_ready()
+{
+    return custom_table_ready;
+}
+
+// Deallocate the custom width table.
+//
+// This does nothing if there is no allocated custom width table, or if
+// there is one but it is in use (marked ready). This is only useful if
+// an initial allocation succeeds, but range population fails, after
+// which point the application may want to deallocate the table to avoid
+// leaving it in an intermediate state.
+void vty_deallocate_custom_table()
+{
+    if ((custom_table != NULL) && (!custom_table_ready)) {
+        free(custom_table);
+        custom_table = NULL;
+        custom_table_size = 0;
+    }
 }
diff --git a/src/Graphics/Text/Width.hs b/src/Graphics/Text/Width.hs
--- a/src/Graphics/Text/Width.hs
+++ b/src/Graphics/Text/Width.hs
@@ -1,6 +1,15 @@
 -- Copyright 2009 Corey O'Connor
 {-# OPTIONS_GHC -D_XOPEN_SOURCE #-}
 {-# LANGUAGE ForeignFunctionInterface #-}
+-- | This module provides functions to measure the terminal column width
+-- of characters and strings.
+--
+-- The functions provided in this module all ultimately make calls to
+-- the C implementation in @cbits/mk_wcwidth.c@. That code manages some
+-- global state that carries a table of Unicode character widths. For
+-- more details, see 'Graphics.Vty.UnicodeWidthTable.Install', the C
+-- code, and the "Multi-Column Character Support" section of the project
+-- @README@.
 module Graphics.Text.Width
   ( wcwidth
   , wcswidth
@@ -33,14 +42,6 @@
 "wcswidth/unpack" forall x. wcswidth (T.unpack x) = wctwidth x
 "wcswidth/lazy-unpack" forall x. wcswidth (TL.unpack x) = wctlwidth x
   #-}
-
--- XXX: Characters with unknown widths occupy 1 column?
---
--- Not sure if this is actually correct. I presume there is a
--- replacement character that is output by the terminal instead of the
--- character and this replacement character is 1 column wide. If this is
--- not true for all terminals then a per-terminal replacement character
--- width needs to be implemented.
 
 -- | Returns the display width of a character. Assumes all characters
 -- with unknown widths are 0 width.
diff --git a/src/Graphics/Vty.hs b/src/Graphics/Vty.hs
--- a/src/Graphics/Vty.hs
+++ b/src/Graphics/Vty.hs
@@ -13,7 +13,7 @@
 --   values in the "Graphics.Vty.Attributes" module.
 --
 -- Vty uses threads internally, so programs made with Vty need to be
--- compiled with the threaded runtime using the GHC `-threaded` option.
+-- compiled with the threaded runtime using the GHC @-threaded@ option.
 --
 -- @
 --  import "Graphics.Vty"
@@ -51,7 +51,10 @@
 import Graphics.Vty.Picture
 import Graphics.Vty.Image
 import Graphics.Vty.Attributes
+import Graphics.Vty.UnicodeWidthTable.IO
+import Graphics.Vty.UnicodeWidthTable.Install
 
+import qualified Control.Exception as E
 import Control.Monad (when)
 import Control.Concurrent.STM
 
@@ -65,7 +68,7 @@
 --
 -- The use of Vty typically follows this process:
 --
---    1. Initialize vty
+--    1. Initialize vty with 'mkVty' (this takes control of the terminal).
 --
 --    2. Use 'update' to display a picture.
 --
@@ -73,31 +76,33 @@
 --
 --    4. Depending on the event, go to 2 or 5.
 --
---    5. Shutdown vty.
+--    5. Shutdown vty and restore the terminal state with 'shutdown'. At
+--    this point the 'Vty' handle cannot be used again.
 --
 -- Operations on Vty handles are not thread-safe.
-data Vty = Vty
-    { -- | Outputs the given 'Picture'.
-      update :: Picture -> IO ()
-      -- | Return the next 'Event' or block until one becomes available.
-    , nextEvent :: IO Event
-      -- | Non-blocking version of 'nextEvent'.
-    , nextEventNonblocking :: IO (Maybe Event)
-      -- | The input interface. See 'Input'.
-    , inputIface :: Input
-      -- | The output interface. See 'Output'.
-    , outputIface :: Output
-      -- | Refresh the display. If other programs output to the terminal
-      -- and mess up the display then the application might want to
-      -- force a refresh using this function.
-    , refresh :: IO ()
-      -- | Clean up after vty. A call to this function is necessary to
-      -- cleanly restore the terminal state before application exit. The
-      -- above methods will throw an exception if executed after this is
-      -- executed. Idempotent.
-    , shutdown :: IO ()
-    , isShutdown :: IO Bool
-    }
+data Vty =
+    Vty { update :: Picture -> IO ()
+        -- ^ Outputs the given 'Picture'.
+        , nextEvent :: IO Event
+        -- ^ Return the next 'Event' or block until one becomes
+        -- available.
+        , nextEventNonblocking :: IO (Maybe Event)
+        -- ^ Non-blocking version of 'nextEvent'.
+        , inputIface :: Input
+        -- ^ The input interface. See 'Input'.
+        , outputIface :: Output
+        -- ^ The output interface. See 'Output'.
+        , refresh :: IO ()
+        -- ^ Refresh the display. If other programs output to the
+        -- terminal and mess up the display then the application might
+        -- want to force a refresh using this function.
+        , shutdown :: IO ()
+        -- ^ Clean up after vty. A call to this function is necessary to
+        -- cleanly restore the terminal state before application exit.
+        -- The above methods will throw an exception if executed after
+        -- this is executed. Idempotent.
+        , isShutdown :: IO Bool
+        }
 
 -- | Create a Vty handle. At most one handle should be created at a time
 -- for a given terminal device.
@@ -110,12 +115,51 @@
 mkVty :: Config -> IO Vty
 mkVty appConfig = do
     config <- (<> appConfig) <$> userConfig
-    input  <- inputForConfig config
-    out    <- outputForConfig config
-    intMkVty input out
 
-intMkVty :: Input -> Output -> IO Vty
-intMkVty input out = do
+    when (allowCustomUnicodeWidthTables config /= Just False) $
+        installCustomWidthTable config
+
+    input <- inputForConfig config
+    out <- outputForConfig config
+    internalMkVty input out
+
+installCustomWidthTable :: Config -> IO ()
+installCustomWidthTable c = do
+    let doLog s = case debugLog c of
+            Nothing -> return ()
+            Just path -> appendFile path $ "installWidthTable: " <> s <> "\n"
+
+    customInstalled <- isCustomTableReady
+    when (not customInstalled) $ do
+        mTerm <- currentTerminalName
+        case mTerm of
+            Nothing ->
+                doLog "No current terminal name available"
+            Just currentTerm ->
+                case lookup currentTerm (termWidthMaps c) of
+                    Nothing ->
+                        doLog "Current terminal not found in custom character width mapping list"
+                    Just path -> do
+                        tableResult <- E.try $ readUnicodeWidthTable path
+                        case tableResult of
+                            Left (e::E.SomeException) ->
+                                doLog $ "Error reading custom character width table " <>
+                                        "at " <> show path <> ": " <> show e
+                            Right (Left msg) ->
+                                doLog $ "Error reading custom character width table " <>
+                                        "at " <> show path <> ": " <> msg
+                            Right (Right table) -> do
+                                installResult <- E.try $ installUnicodeWidthTable table
+                                case installResult of
+                                    Left (e::E.SomeException) ->
+                                        doLog $ "Error installing unicode table (" <>
+                                                show path <> ": " <> show e
+                                    Right () ->
+                                        doLog $ "Successfully installed Unicode width table " <>
+                                                " from " <> show path
+
+internalMkVty :: Input -> Output -> IO Vty
+internalMkVty input out = do
     reserveDisplay out
 
     shutdownVar <- atomically $ newTVar False
diff --git a/src/Graphics/Vty/Config.hs b/src/Graphics/Vty/Config.hs
--- a/src/Graphics/Vty/Config.hs
+++ b/src/Graphics/Vty/Config.hs
@@ -55,6 +55,32 @@
 -- table to the log in the above format. A workflow for using this is
 -- to set @VTY_DEBUG_LOG@. Run the application. Check the debug log for
 -- incorrect mappings. Add corrected mappings to @$HOME/.vty/config@.
+--
+-- = Unicode Character Width Maps
+--
+-- == @widthMap@
+--
+-- Format:
+--
+-- @
+--  \"widthMap\" string string
+-- @
+--
+-- E.g.,
+--
+-- @
+--   widthMap \"xterm\" \"\/home\/user\/.vty\/xterm\_map.dat\"
+-- @
+--
+-- This directive specifies the path to a Unicode character width
+-- map (the second argument) that should be loaded and used when
+-- the value of TERM matches the first argument. Unicode character
+-- width maps can be produced either by running the provided binary
+-- @vty-build-width-table@ or by calling the library routine
+-- 'Graphics.Vty.UnicodeWidthTable.Query.buildUnicodeWidthTable'. The
+-- 'Graphics.Vty.mkVty' function will use these configuration settings
+-- to attempt to load and install the specified width map. See the
+-- documentation for 'Graphics.Vty.mkVty' for details.
 module Graphics.Vty.Config
   ( InputMap
   , Config(..)
@@ -66,6 +92,16 @@
   , parseConfigFile
   , defaultConfig
   , getTtyEraseChar
+  , currentTerminalName
+
+  , vtyConfigPath
+  , widthTableFilename
+  , vtyDataDirectory
+  , terminalWidthTablePath
+  , vtyConfigFileEnvName
+
+  , ConfigUpdateResult(..)
+  , addConfigWidthMap
   )
 where
 
@@ -89,8 +125,11 @@
 
 import GHC.Generics
 
-import System.Directory (getAppUserDataDirectory)
+import System.Directory ( getAppUserDataDirectory, doesFileExist
+                        , createDirectoryIfMissing
+                        )
 import System.Environment (lookupEnv)
+import System.FilePath ((</>), takeDirectory)
 import System.Posix.IO (stdInput, stdOutput)
 import System.Posix.Types (Fd(..))
 import Foreign.C.Types (CInt(..), CChar(..))
@@ -100,12 +139,13 @@
 import qualified Text.Parsec.Token as P
 
 -- | Type of errors that can be thrown when configuring VTY
-data VtyConfigurationError
-  = VtyMissingTermEnvVar -- ^ TERM environment variable not set
-  deriving (Show, Eq, Typeable)
+data VtyConfigurationError =
+    VtyMissingTermEnvVar
+    -- ^ TERM environment variable not set
+    deriving (Show, Eq, Typeable)
 
 instance Exception VtyConfigurationError where
-  displayException VtyMissingTermEnvVar = "TERM environment variable not set"
+    displayException VtyMissingTermEnvVar = "TERM environment variable not set"
 
 -- | Mappings from input bytes to event in the order specified. Later
 -- entries take precedence over earlier in the case multiple entries
@@ -113,77 +153,125 @@
 type InputMap = [(Maybe String, String, Event)]
 
 -- | A Vty configuration.
-data Config = Config
-    {
-    -- | The default is 1 character.
-      vmin  :: Maybe Int
-    -- | The default is 100 milliseconds, 0.1 seconds.
-    , vtime :: Maybe Int
-    -- | The default is False.
-    , mouseMode :: Maybe Bool
-    -- | The default is False.
-    , bracketedPasteMode :: Maybe Bool
-    -- | Debug information is appended to this file if not Nothing.
-    , debugLog           :: Maybe FilePath
-    -- | The (input byte, output event) pairs extend the internal input
-    -- table of VTY and the table from terminfo.
-    --
-    -- See "Graphics.Vty.Config" module documentation for documentation
-    -- of the @map@ directive.
-    , inputMap           :: InputMap
-    -- | The input file descriptor to use. The default is
-    -- 'System.Posix.IO.stdInput'
-    , inputFd           :: Maybe Fd
-    -- | The output file descriptor to use. The default is
-    -- 'System.Posix.IO.stdOutput'
-    , outputFd          :: Maybe Fd
-    -- | The terminal name used to look up terminfo capabilities.
-    -- The default is the value of the TERM environment variable.
-    , termName           :: Maybe String
-    } deriving (Show, Eq)
+data Config =
+    Config { vmin  :: Maybe Int
+           -- ^ The default is 1 character.
+           , vtime :: Maybe Int
+           -- ^ The default is 100 milliseconds, 0.1 seconds.
+           , mouseMode :: Maybe Bool
+           -- ^ The default is False.
+           , bracketedPasteMode :: Maybe Bool
+           -- ^ The default is False.
+           , debugLog :: Maybe FilePath
+           -- ^ Debug information is appended to this file if not
+           -- Nothing.
+           , inputMap :: InputMap
+           -- ^ The (input byte, output event) pairs extend the internal
+           -- input table of VTY and the table from terminfo.
+           --
+           -- See "Graphics.Vty.Config" module documentation for
+           -- documentation of the @map@ directive.
+           , inputFd :: Maybe Fd
+           -- ^ The input file descriptor to use. The default is
+           -- 'System.Posix.IO.stdInput'
+           , outputFd :: Maybe Fd
+           -- ^ The output file descriptor to use. The default is
+           -- 'System.Posix.IO.stdOutput'
+           , termName :: Maybe String
+           -- ^ The terminal name used to look up terminfo capabilities.
+           -- The default is the value of the TERM environment variable.
+           , termWidthMaps :: [(String, FilePath)]
+           -- ^ Terminal width map files.
+           , allowCustomUnicodeWidthTables :: Maybe Bool
+           -- ^ Whether to permit custom Unicode width table loading by
+           -- 'Graphics.Vty.mkVty'. @'Just' 'False'@ indicates that
+           -- table loading should not be performed. Other values permit
+           -- table loading.
+           --
+           -- If a table load is attempted and fails, information
+           -- about the failure will be logged to the debug log if the
+           -- configuration specifies one. If no custom table is loaded
+           -- (or if a load fails), the built-in character width table
+           -- will be used.
+           }
+           deriving (Show, Eq)
 
 defaultConfig :: Config
 defaultConfig = mempty
 
 instance Semigroup Config where
-    c0 <> c1 = Config
+    c0 <> c1 =
         -- latter config takes priority for everything but inputMap
-        { vmin          = vmin c1     <|> vmin c0
-        , vtime         = vtime c1    <|> vtime c0
-        , mouseMode     = mouseMode c1
-        , bracketedPasteMode = bracketedPasteMode c1
-        , debugLog      = debugLog c1 <|> debugLog c0
-        , inputMap      = inputMap c0 <>  inputMap c1
-        , inputFd      = inputFd c1 <|> inputFd c0
-        , outputFd     = outputFd c1 <|> outputFd c0
-        , termName      = termName c1 <|> termName c0
-        }
+        Config { vmin = vmin c1 <|> vmin c0
+               , vtime = vtime c1 <|> vtime c0
+               , mouseMode = mouseMode c1
+               , bracketedPasteMode = bracketedPasteMode c1
+               , debugLog = debugLog c1 <|> debugLog c0
+               , inputMap = inputMap c0 <> inputMap c1
+               , inputFd = inputFd c1 <|> inputFd c0
+               , outputFd = outputFd c1 <|> outputFd c0
+               , termName = termName c1 <|> termName c0
+               , termWidthMaps = termWidthMaps c1 <|> termWidthMaps c0
+               , allowCustomUnicodeWidthTables =
+                   allowCustomUnicodeWidthTables c1 <|> allowCustomUnicodeWidthTables c0
+               }
 
 instance Monoid Config where
-    mempty = Config
-        { vmin         = Nothing
-        , vtime        = Nothing
-        , mouseMode    = Nothing
-        , bracketedPasteMode = Nothing
-        , debugLog     = mempty
-        , inputMap     = mempty
-        , inputFd     = Nothing
-        , outputFd    = Nothing
-        , termName     = Nothing
-        }
+    mempty =
+        Config { vmin = Nothing
+               , vtime = Nothing
+               , mouseMode = Nothing
+               , bracketedPasteMode = Nothing
+               , debugLog = mempty
+               , inputMap = mempty
+               , inputFd = Nothing
+               , outputFd = Nothing
+               , termName = Nothing
+               , termWidthMaps = []
+               , allowCustomUnicodeWidthTables = Nothing
+               }
 #if !(MIN_VERSION_base(4,11,0))
     mappend = (<>)
 #endif
 
--- | Load a configuration from @'getAppUserDataDirectory'/config@ and
--- @$VTY_CONFIG_FILE@.
+vtyDataDirectory :: IO FilePath
+vtyDataDirectory = getAppUserDataDirectory "vty"
+
+vtyConfigPath :: IO FilePath
+vtyConfigPath = do
+    dir <- vtyDataDirectory
+    return $ dir </> "config"
+
+vtyConfigFileEnvName :: String
+vtyConfigFileEnvName = "VTY_CONFIG_FILE"
+
+-- | Load a configuration from 'vtyConfigPath' and @$VTY_CONFIG_FILE@.
 userConfig :: IO Config
 userConfig = do
-    configFile <- (mappend <$> getAppUserDataDirectory "vty" <*> pure "/config") >>= parseConfigFile
-    overrideConfig <- maybe (return defaultConfig) parseConfigFile =<< lookupEnv "VTY_CONFIG_FILE"
+    configFile <- vtyConfigPath >>= parseConfigFile
+    overrideConfig <- maybe (return defaultConfig) parseConfigFile =<<
+        lookupEnv vtyConfigFileEnvName
     let base = configFile <> overrideConfig
     mappend base <$> overrideEnvConfig
 
+widthTableFilename :: String -> String
+widthTableFilename term = "width_table_" <> term <> ".dat"
+
+termVariable :: String
+termVariable = "TERM"
+
+currentTerminalName :: IO (Maybe String)
+currentTerminalName = lookupEnv termVariable
+
+terminalWidthTablePath :: IO (Maybe FilePath)
+terminalWidthTablePath = do
+    dataDir <- vtyDataDirectory
+    result <- lookupEnv termVariable
+    case result of
+        Nothing -> return Nothing
+        Just term -> do
+            return $ Just $ dataDir </> widthTableFilename term
+
 overrideEnvConfig :: IO Config
 overrideEnvConfig = do
     d <- lookupEnv "VTY_DEBUG_LOG"
@@ -193,7 +281,7 @@
 -- can raise 'VtyConfigurationError'.
 standardIOConfig :: IO Config
 standardIOConfig = do
-    mb <- lookupEnv "TERM"
+    mb <- lookupEnv termVariable
     case mb of
       Nothing -> throwIO VtyMissingTermEnvVar
       Just t ->
@@ -256,13 +344,20 @@
     path       <- P.stringLiteral configLexer
     return defaultConfig { debugLog = Just path }
 
+widthMapDecl :: Parser Config
+widthMapDecl = do
+    "widthMap" <- P.identifier configLexer
+    tName <- P.stringLiteral configLexer
+    path <- P.stringLiteral configLexer
+    return defaultConfig { termWidthMaps = [(tName, path)] }
+
 ignoreLine :: Parser ()
 ignoreLine = void $ manyTill anyChar newline
 
 parseConfig :: Parser Config
 parseConfig = liftM mconcat $ many $ do
     P.whiteSpace configLexer
-    let directives = [try mapDecl, try debugLogDecl]
+    let directives = [try mapDecl, try debugLogDecl, try widthMapDecl]
     choice directives <|> (ignoreLine >> return defaultConfig)
 
 class    Parse a        where parseValue :: Parser a
@@ -329,3 +424,56 @@
     if c /= 0
        then return $ Just $ toEnum $ fromEnum c
        else return Nothing
+
+data ConfigUpdateResult =
+    ConfigurationCreated
+    | ConfigurationModified
+    | ConfigurationConflict String
+    | ConfigurationRedundant
+    deriving (Eq, Show)
+
+-- | Add a @widthMap@ directive to the Vty configuration file at the
+-- specified path.
+--
+-- If the configuration path refers to a configuration that already
+-- contains the directive for the specified map and terminal type, the
+-- configuration file will not be modified. If the file does not contain
+-- the directive, it will be appended to the file.
+--
+-- If the configuration path does not exist, a new configuration file
+-- will be created and any directories in the path will also be created.
+--
+-- This returns @True@ if the configuration was created or modified and
+-- @False@ otherwise. This does not handle exceptions raised by file or
+-- directory permissions issues.
+addConfigWidthMap :: FilePath
+                  -- ^ The configuration file path of the configuration
+                  -- to modify or create.
+                  -> String
+                  -- ^ The @TERM@ value for the @widthMap@ directive.
+                  -> FilePath
+                  -- ^ The width table file path for the directive.
+                  -> IO ConfigUpdateResult
+addConfigWidthMap configPath term tablePath = do
+    configEx <- doesFileExist configPath
+    if configEx
+       then updateConfig
+       else createConfig >> return ConfigurationCreated
+
+    where
+        directive = "widthMap " <> show term <> " " <> show tablePath <> "\n"
+
+        createConfig = do
+            let dir = takeDirectory configPath
+            createDirectoryIfMissing True dir
+            writeFile configPath directive
+
+        updateConfig = do
+            config <- parseConfigFile configPath
+            if (term, tablePath) `elem` termWidthMaps config
+               then return ConfigurationRedundant
+               else case lookup term (termWidthMaps config) of
+                   Just other -> return $ ConfigurationConflict other
+                   Nothing -> do
+                       appendFile configPath directive
+                       return ConfigurationModified
diff --git a/src/Graphics/Vty/UnicodeWidthTable/IO.hs b/src/Graphics/Vty/UnicodeWidthTable/IO.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/UnicodeWidthTable/IO.hs
@@ -0,0 +1,105 @@
+{-# LANGUAGE CPP #-}
+module Graphics.Vty.UnicodeWidthTable.IO
+  ( readUnicodeWidthTable
+  , parseUnicodeWidthTable
+  , writeUnicodeWidthTable
+  )
+where
+
+import Control.Monad (when, forM)
+import Data.Binary
+import Data.Binary.Get
+import Data.Binary.Put
+import qualified Data.ByteString.Lazy as BSL
+#if !(MIN_VERSION_base(4,11,0))
+import Data.Semigroup ((<>))
+#endif
+
+import Graphics.Vty.UnicodeWidthTable.Types
+
+-- | Load a binary unicode width table from the specified file.
+--
+-- This either returns a successfully parsed table or a table parsing
+-- error message. This does not handle I/O exceptions.
+readUnicodeWidthTable :: FilePath -> IO (Either String UnicodeWidthTable)
+readUnicodeWidthTable path = parseUnicodeWidthTable <$> BSL.readFile path
+
+-- | Parse a binary unicode width table.
+parseUnicodeWidthTable :: BSL.ByteString -> Either String UnicodeWidthTable
+parseUnicodeWidthTable bs =
+    case runGetOrFail tableParser bs of
+        Left (_, _, msg) ->
+            Left msg
+
+        -- Even if we parsed a table, leftover bytes indicate something
+        -- could be wrong.
+        Right (remainingBytes, _, _) | not (BSL.null remainingBytes) ->
+            Left $ "Error: " <> show (BSL.length remainingBytes) <>
+                   " byte(s) left unconsumed"
+
+        Right (_, _, table) ->
+            Right table
+
+-- | Write the unicode width table to the specified path.
+--
+-- This does not handle I/O exceptions.
+writeUnicodeWidthTable :: FilePath -> UnicodeWidthTable -> IO ()
+writeUnicodeWidthTable path table = do
+    let body = runPut (tableV1Writer table)
+    BSL.writeFile path body
+
+-- | Width table magic bytes for use in the binary format.
+widthTableMagic :: Word32
+widthTableMagic = 0xc1a9f7e0
+
+tableParser :: Get UnicodeWidthTable
+tableParser = do
+    magic <- getWord32le
+
+    when (magic /= widthTableMagic) $
+        fail "Table magic number invalid"
+
+    version <- getWord8
+
+    case version of
+        1 -> tableV1Parser
+        _ -> fail "Table version invalid"
+
+tableV1Parser :: Get UnicodeWidthTable
+tableV1Parser = do
+    numRanges <- getWord32le
+
+    let parseRange = do
+            start <- getWord32le
+            size <- getWord32le
+            cols <- getWord8
+            return WidthTableRange { rangeStart = start
+                                   , rangeSize = size
+                                   , rangeColumns = cols
+                                   }
+
+    ranges <- forM [1..numRanges] $ const parseRange
+
+    return UnicodeWidthTable { unicodeWidthTableRanges = ranges
+                             }
+
+tableV1Writer :: UnicodeWidthTable -> Put
+tableV1Writer table = do
+    -- Magic bytes
+    putWord32le widthTableMagic
+
+    -- Version
+    putWord8 1
+
+    -- Number of ranges
+    let ranges = unicodeWidthTableRanges table
+    let numRanges = length ranges
+    putWord32le (fromIntegral numRanges)
+
+    -- Ranges
+    let putRange r = do
+            putWord32le $ rangeStart r
+            putWord32le $ rangeSize r
+            putWord8 $ rangeColumns r
+
+    mapM_ putRange ranges
diff --git a/src/Graphics/Vty/UnicodeWidthTable/Install.hs b/src/Graphics/Vty/UnicodeWidthTable/Install.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/UnicodeWidthTable/Install.hs
@@ -0,0 +1,135 @@
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE ForeignFunctionInterface #-}
+module Graphics.Vty.UnicodeWidthTable.Install
+  ( TableInstallException(..)
+  , installUnicodeWidthTable
+  , isCustomTableReady
+  )
+where
+
+import Control.Monad (when, forM_)
+import qualified Control.Exception as E
+import Control.Concurrent.MVar (MVar, newMVar, takeMVar, putMVar)
+import Data.Word (Word8, Word32)
+#if !(MIN_VERSION_base(4,11,0))
+import Data.Semigroup ((<>))
+#endif
+import System.IO.Unsafe (unsafePerformIO)
+
+import Graphics.Vty.UnicodeWidthTable.Types
+
+-- | The lock used to make functions in this module thread-safe.
+installLock :: MVar ()
+{-# NOINLINE installLock #-}
+installLock = unsafePerformIO $ newMVar ()
+
+-- | Returns True if and only if a custom table has been allocated and
+-- marked as ready for use.
+--
+-- This function is thread-safe.
+isCustomTableReady :: IO Bool
+isCustomTableReady = withInstallLock $ (== 1) <$> c_isCustomTableReady
+
+withInstallLock :: IO a -> IO a
+withInstallLock act =
+    E.bracket takeInstallLock (const releaseInstallLock) $ const act
+
+takeInstallLock :: IO ()
+takeInstallLock = takeMVar installLock
+
+releaseInstallLock :: IO ()
+releaseInstallLock = putMVar installLock ()
+
+-- This is the size of the allocated custom character width table, in
+-- character slots. It's important that this be large enough to hold all
+-- possible Unicode character values. At the time of this writing, the
+-- valid Unicode range is 0 - 0x10ffff, hence this value.
+tableSize :: Int
+tableSize = 0x110000
+
+-- | Exception type raised by 'installUnicodeWidthTable'.
+data TableInstallException =
+    TableInitFailure Int Int
+    -- ^ The width table could not be initialized. Args: failure status,
+    -- requested table size.
+    | TableRangeFailure Int WidthTableRange
+    -- ^ A code point range could not be configured. Args: failure
+    -- status, offending range.
+    | TableActivationFailure Int
+    -- ^ The table could not be activated. Args: failure status.
+    deriving (Eq, Show)
+
+instance E.Exception TableInstallException
+
+-- | Install a custom unicode character width
+-- table. Such tables are obtained with
+-- 'Graphics.Vty.UnicodeWidthTable.Query.buildUnicodeWidthTable' and
+-- 'Graphics.Vty.UnicodeWidthTable.IO.readUnicodeWidthTable'.
+--
+-- ALERT! This function is probably not what you want to use because
+-- it is automatically called by 'Graphics.Vty.mkVty'. You will only
+-- ever need to call this function if you want to use functions
+-- in 'Graphics.Text.Width' without controlling the terminal with
+-- 'Graphics.Vty.mkVty'.
+--
+-- This affects the behavior of the 'Graphics.Vty.Image.wcwidth'
+-- function and functions that call it. It does so by
+-- changing global state available to the C implementation
+-- of 'Graphics.Vty.Image.wcwidth'. To ensure that your
+-- program gets consistent results from evaluating calls to
+-- 'Graphics.Vty.Image.wcwidth', the installation of a custom table
+-- should be performed before you call 'Graphics.Vty.Image.wcwidth' in
+-- your program.
+--
+-- This is best done at Vty startup, and if you use
+-- 'Graphics.Vty.mkVty', that function calls this automatically based on
+-- the Vty configuration's declared width tables. It is exposed as part
+-- of the public API so that applications can call this as needed if
+-- they don't want to control the terminal with 'mkVty' but do want to
+-- make calls to 'Graphics.Vty.Image.wcwidth'.
+--
+-- It's also important to note that once a custom table has been
+-- installed, it is permanent for the life of the process. No new table
+-- can be installed, and the new custom table cannot be removed.
+--
+-- If this function fails for any reason -- if the table cannot be
+-- installed or is invalid, or if a custom table already exists -- this
+-- will raise a 'TableInstallException' exception.
+--
+-- This function is thread-safe.
+installUnicodeWidthTable :: UnicodeWidthTable -> IO ()
+installUnicodeWidthTable table = withInstallLock $ do
+    initResult <- initCustomTable tableSize
+    when (initResult /= 0) $
+        E.throw $ TableInitFailure initResult tableSize
+
+    forM_ (unicodeWidthTableRanges table) $ \r -> do
+        result <- setCustomTableRange (rangeStart r)
+                                      (rangeSize r)
+                                      (rangeColumns r)
+
+        when (result /= 0) $ do
+            deallocateCustomTable
+            E.throw $ TableRangeFailure result r
+
+    actResult <- activateCustomTable
+    when (actResult /= 0) $
+        E.throw $ TableActivationFailure actResult
+
+------------------------------------------------------------------------
+-- C imports
+
+foreign import ccall unsafe "vty_init_custom_table"
+    initCustomTable :: Int -> IO Int
+
+foreign import ccall unsafe "vty_set_custom_table_range"
+    setCustomTableRange :: Word32 -> Word32 -> Word8 -> IO Int
+
+foreign import ccall unsafe "vty_activate_custom_table"
+    activateCustomTable :: IO Int
+
+foreign import ccall unsafe "vty_custom_table_ready"
+    c_isCustomTableReady :: IO Int
+
+foreign import ccall unsafe "vty_deallocate_custom_table"
+    deallocateCustomTable :: IO ()
diff --git a/src/Graphics/Vty/UnicodeWidthTable/Query.hs b/src/Graphics/Vty/UnicodeWidthTable/Query.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/UnicodeWidthTable/Query.hs
@@ -0,0 +1,79 @@
+{-# LANGUAGE TupleSections #-}
+module Graphics.Vty.UnicodeWidthTable.Query
+  ( buildUnicodeWidthTable
+  , defaultUnicodeTableUpperBound
+  )
+where
+
+import Control.Monad (forM)
+import Data.Char (generalCategory, GeneralCategory(..))
+import System.Console.ANSI (getCursorPosition)
+import Text.Printf (printf)
+
+import Graphics.Vty.UnicodeWidthTable.Types
+
+shouldConsider :: Char -> Bool
+shouldConsider c =
+    case generalCategory c of
+        Control     -> False
+        NotAssigned -> False
+        Surrogate   -> False
+        _           -> True
+
+charWidth :: Char -> IO Int
+charWidth c = do
+    printf "\r"
+    putChar c
+    Just (_, col) <- getCursorPosition
+    return col
+
+-- | Convert a sequence of character/width pairs into a list of
+-- run-length encoded ranges. This function assumes the pairs come
+-- sorted by character ordinal value. It does not require that the
+-- character range is fully covered by the sequence.
+--
+-- The result of this function is a list of ranges in reverse order
+-- relative to the input sequence.
+mkRanges :: [(Char, Int)] -> [WidthTableRange]
+mkRanges pairs =
+    let convertedPairs = convert <$> pairs
+        convert (c, i) = (fromIntegral $ fromEnum c, fromIntegral i)
+
+        go Nothing finishedRanges [] = finishedRanges
+        go (Just r) finishedRanges [] = r:finishedRanges
+
+        go Nothing finishedRanges ((c, width):rest) =
+            go (Just $ WidthTableRange c 1 width) finishedRanges rest
+
+        go (Just r@(WidthTableRange prevCh sz prevWidth)) finishedRanges ((c, width):rest) =
+            if c == prevCh + sz && prevWidth == width
+            then go (Just (WidthTableRange prevCh (sz + 1) prevWidth)) finishedRanges rest
+            else go (Just (WidthTableRange c 1 width)) (r:finishedRanges) rest
+
+    in go Nothing [] convertedPairs
+
+-- The uppermost code point to consider when building Unicode width
+-- tables.
+defaultUnicodeTableUpperBound :: Char
+defaultUnicodeTableUpperBound = '\xe0000'
+
+-- | Construct a unicode character width table by querying the terminal
+-- connected to stdout. This works by emitting characters to stdout
+-- and then querying the terminal to determine the resulting cursor
+-- position in order to measure character widths. Consequently this will
+-- generate a lot of output and may take a while, depending on your
+-- system performance. This should not be run in a terminal while it is
+-- controlled by Vty.
+--
+-- The argument specifies the upper bound code point to test when
+-- building the table. This allows callers to decide how much of the
+-- Unicode code point space to scan when building the table.
+--
+-- This does not handle exceptions.
+buildUnicodeWidthTable :: Char -> IO UnicodeWidthTable
+buildUnicodeWidthTable tableUpperBound = do
+    pairs <- forM (filter shouldConsider ['\0'..tableUpperBound]) $ \i ->
+        (i,) <$> charWidth i
+
+    return UnicodeWidthTable { unicodeWidthTableRanges = reverse $ mkRanges pairs
+                             }
diff --git a/src/Graphics/Vty/UnicodeWidthTable/Types.hs b/src/Graphics/Vty/UnicodeWidthTable/Types.hs
new file mode 100644
--- /dev/null
+++ b/src/Graphics/Vty/UnicodeWidthTable/Types.hs
@@ -0,0 +1,29 @@
+module Graphics.Vty.UnicodeWidthTable.Types
+  ( UnicodeWidthTable(..)
+  , WidthTableRange(..)
+  )
+where
+
+import Data.Word (Word8, Word32)
+
+-- | A range of code points in a width table.
+data WidthTableRange =
+    WidthTableRange { rangeStart :: Word32
+                    -- ^ The range's starting code point.
+                    , rangeSize :: Word32
+                    -- ^ The number of code points in the contiguous
+                    -- range, beginning with the starting code point
+                    -- ('rangeStart').
+                    , rangeColumns :: Word8
+                    -- ^ The terminal width, in columns, of all of the
+                    -- characters corresponding to the code points in
+                    -- this range.
+                    }
+    deriving (Eq, Show)
+
+-- | A run-length-encoded table of Unicode character widths.
+data UnicodeWidthTable =
+    UnicodeWidthTable { unicodeWidthTableRanges :: [WidthTableRange]
+                      -- ^ The ranges in the table.
+                      }
+    deriving (Show)
diff --git a/tools/BuildWidthTable.hs b/tools/BuildWidthTable.hs
new file mode 100644
--- /dev/null
+++ b/tools/BuildWidthTable.hs
@@ -0,0 +1,144 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE CPP #-}
+module Main where
+
+import qualified Control.Exception as E
+import Control.Monad (when)
+import Data.Maybe (fromMaybe)
+#if !(MIN_VERSION_base(4,11,0))
+import Data.Semigroup ((<>))
+#endif
+import System.Directory (createDirectoryIfMissing)
+import System.Environment (getArgs, getProgName)
+import System.FilePath (takeDirectory)
+import System.Exit (exitFailure)
+import System.Console.GetOpt
+import Text.Read (readMaybe)
+
+import Graphics.Vty.Config ( terminalWidthTablePath, currentTerminalName
+                           , vtyConfigPath, addConfigWidthMap
+                           , ConfigUpdateResult(..)
+                           )
+import Graphics.Vty.UnicodeWidthTable.IO
+import Graphics.Vty.UnicodeWidthTable.Query
+
+data Arg = Help
+         | OutputPath String
+         | TableUpperBound String
+         | UpdateConfig
+         | VtyConfigPath String
+         deriving (Eq, Show)
+
+options :: Config -> [OptDescr Arg]
+options config =
+    [ Option "h" ["help"] (NoArg Help)
+      "This help output"
+    , Option "b" ["bound"] (ReqArg TableUpperBound "MAX_CHAR")
+      ("The maximum Unicode code point to test when building the table " <>
+       "(default: " <> (show $ fromEnum $ configBound config) <> ")")
+    , Option "p" ["path"] (ReqArg OutputPath "PATH")
+      ("The output path to write to (default: " <>
+       fromMaybe "<none>" (configOutputPath config) <> ")")
+    , Option "u" ["update-config"] (NoArg UpdateConfig)
+      "Create or update the Vty configuration file to use the new table (default: no)"
+    , Option "c" ["config-path"] (ReqArg VtyConfigPath "PATH")
+      ("Update the specified Vty configuration file path when -u is set (default: " <>
+       configPath config <> ")")
+    ]
+
+data Config =
+    Config { configOutputPath :: Maybe FilePath
+           , configBound :: Char
+           , configUpdate :: Bool
+           , configPath :: FilePath
+           }
+           deriving (Show)
+
+mkDefaultConfig :: IO Config
+mkDefaultConfig = do
+    Config <$> terminalWidthTablePath
+           <*> pure defaultUnicodeTableUpperBound
+           <*> pure False
+           <*> vtyConfigPath
+
+usage :: IO ()
+usage = do
+    config <- mkDefaultConfig
+    pn <- getProgName
+    putStrLn $ "Usage: " <> pn <> " [options]"
+    putStrLn ""
+    putStrLn "This tool queries the terminal on stdout to determine the widths"
+    putStrLn "of Unicode characters rendered to the terminal. The resulting data"
+    putStrLn "is written to a table at the specified output path for later"
+    putStrLn "loading by Vty-based applications."
+    putStrLn ""
+
+    putStrLn $ usageInfo pn (options config)
+
+updateConfigFromArg :: Arg -> Config -> Config
+updateConfigFromArg Help c =
+    c
+updateConfigFromArg UpdateConfig c =
+    c { configUpdate = True }
+updateConfigFromArg (VtyConfigPath p) c =
+    c { configPath = p }
+updateConfigFromArg (TableUpperBound s) c =
+    case readMaybe s of
+        Nothing -> error $ "Invalid table upper bound: " <> show s
+        Just v  -> c { configBound = toEnum v }
+updateConfigFromArg (OutputPath p) c =
+    c { configOutputPath = Just p }
+
+main :: IO ()
+main = do
+    defConfig <- mkDefaultConfig
+    strArgs <- getArgs
+    let (args, unused, errors) = getOpt Permute (options defConfig) strArgs
+
+    when (not $ null errors) $ do
+        mapM_ putStrLn errors
+        exitFailure
+
+    when ((not $ null unused) || (Help `elem` args)) $ do
+        usage
+        exitFailure
+
+    let config = foldr updateConfigFromArg defConfig args
+
+    outputPath <- case configOutputPath config of
+        Nothing -> do
+            putStrLn "Error: could not obtain terminal width table path"
+            exitFailure
+        Just path -> return path
+
+    putStrLn "Querying terminal:"
+    builtTable <- buildUnicodeWidthTable $ configBound config
+
+    let dir = takeDirectory outputPath
+    createDirectoryIfMissing True dir
+    writeUnicodeWidthTable outputPath builtTable
+
+    putStrLn $ "\nOutput table written to " <> outputPath
+
+    when (configUpdate config) $ do
+        let cPath = configPath config
+        Just tName <- currentTerminalName
+
+        result <- E.try $ addConfigWidthMap cPath tName outputPath
+
+        case result of
+            Left (e::E.SomeException) -> do
+                putStrLn $ "Error updating Vty configuration at " <> cPath <> ": " <>
+                           show e
+                exitFailure
+            Right ConfigurationCreated -> do
+                putStrLn $ "Configuration file created: " <> cPath
+            Right ConfigurationModified -> do
+                putStrLn $ "Configuration file updated: " <> cPath
+            Right (ConfigurationConflict other) -> do
+                putStrLn $ "Configuration file not updated: uses a different table " <>
+                           "for TERM=" <> tName <> ": " <> other
+            Right ConfigurationRedundant -> do
+                putStrLn $ "Configuration file not updated: configuration " <>
+                           cPath <> " already uses table " <> outputPath <>
+                           " for TERM=" <> tName
diff --git a/vty.cabal b/vty.cabal
--- a/vty.cabal
+++ b/vty.cabal
@@ -1,5 +1,5 @@
 name:                vty
-version:             5.27
+version:             5.28
 license:             BSD3
 license-file:        LICENSE
 author:              AUTHORS
@@ -48,7 +48,6 @@
                        microlens < 0.4.12,
                        microlens-mtl,
                        microlens-th,
-                       -- required for nice installation with yi
                        hashable >= 1.2,
                        mtl >= 1.1.1.0 && < 2.3,
                        parallel >= 2.2 && < 3.3,
@@ -59,7 +58,9 @@
                        text >= 0.11.3,
                        unix,
                        utf8-string >= 0.3 && < 1.1,
-                       vector >= 0.7
+                       vector >= 0.7,
+                       binary,
+                       ansi-terminal >= 0.10.3
 
   if !impl(ghc >= 8.0)
     build-depends:     semigroups >= 0.16,
@@ -78,7 +79,6 @@
                        Graphics.Vty.Picture
                        Graphics.Vty.Output
                        Graphics.Text.Width
-                       -- the modules below are only meant to be used by the tests.
                        Codec.Binary.UTF8.Debug
                        Data.Terminfo.Parse
                        Data.Terminfo.Eval
@@ -99,6 +99,10 @@
                        Graphics.Vty.Output.Interface
                        Graphics.Vty.Output.XTermColor
                        Graphics.Vty.Output.TerminfoBased
+                       Graphics.Vty.UnicodeWidthTable.Types
+                       Graphics.Vty.UnicodeWidthTable.IO
+                       Graphics.Vty.UnicodeWidthTable.Query
+                       Graphics.Vty.UnicodeWidthTable.Install
 
   other-modules:       Graphics.Vty.Attributes.Color240
                        Graphics.Vty.Debug.Image
@@ -114,11 +118,22 @@
   hs-source-dirs:      src
 
   default-extensions:  ScopedTypeVariables
-                       ForeignFunctionInterface
 
   ghc-options:         -O2 -funbox-strict-fields -Wall -fspec-constr -fspec-constr-count=10
 
   ghc-prof-options:    -O2 -funbox-strict-fields -caf-all -Wall -fspec-constr -fspec-constr-count=10
+
+executable vty-build-width-table
+  main-is:             BuildWidthTable.hs
+  hs-source-dirs:      tools
+
+  default-language:    Haskell2010
+  ghc-options:         -threaded -Wall
+
+  build-depends:       vty,
+                       directory,
+                       filepath,
+                       base >= 4.8 && < 5
 
 executable vty-mode-demo
   main-is:             ModeDemo.hs
