diff --git a/gdb/Makefile.in b/gdb/Makefile.in
index 448a495bb3b..adfe607dc87 100644
--- a/gdb/Makefile.in
+++ b/gdb/Makefile.in
@@ -401,6 +401,7 @@ SUBDIR_PYTHON_SRCS = \
 	python/py-symbol.c \
 	python/py-symtab.c \
 	python/py-threadevent.c \
+	python/py-tui.c \
 	python/py-type.c \
 	python/py-unwind.c \
 	python/py-utils.c \
diff --git a/gdb/NEWS b/gdb/NEWS
index a936620c0a8..f15c5f5c15f 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -15,6 +15,11 @@ tui new-layout NAME WINDOW WEIGHT [WINDOW WEIGHT]...
   Define a new TUI layout, specifying its name and the windows that
   will be displayed.
 
+* Python API
+
+  ** gdb.register_window_type can be used to implement new TUI windows
+     in Python.
+
 *** Changes in GDB 9
 
 * 'thread-exited' event is now available in the annotations interface.
diff --git a/gdb/doc/python.texi b/gdb/doc/python.texi
index 8124077ab33..d0ce9ea35e7 100644
--- a/gdb/doc/python.texi
+++ b/gdb/doc/python.texi
@@ -163,6 +163,7 @@ optional arguments while skipping others.  Example:
                                 using Python.
 * Lazy Strings In Python::      Python representation of lazy strings.
 * Architectures In Python::     Python representation of architectures.
+* TUI Windows In Python::       Implementing new TUI windows.
 @end menu
 
 @node Basic Python
@@ -5673,6 +5674,106 @@ instruction in bytes.
 @end table
 @end defun
 
+@node TUI Windows In Python
+@subsubsection Implementing new TUI windows
+@cindex Python TUI Windows
+
+New TUI (@pxref{TUI}) windows can be implemented in Python.
+
+@findex gdb.register_window_type
+@defun gdb.register_window_type (@var{name}, @var{factory})
+Because TUI windows are created and destroyed depending on the layout
+the user chooses, new window types are implemented by registering a
+factory function with @value{GDBN}.
+
+@var{name} is the name of the new window.  It's an error to try to
+replace one of the built-in windows, but other window types can be
+replaced.
+
+@var{function} is a factory function that is called to create the TUI
+window.  This is called with a single argument of type
+@code{gdb.TuiWindow}, described below.  It should return an object
+that implements the TUI window protocol, also described below.
+@end defun
+
+As mentioned above, when a factory function is called, it is passed a
+an object of type @code{gdb.TuiWindow}.  This object has these
+methods and attributes:
+
+@defun TuiWindow.is_valid ()
+This method returns @code{True} when this window is valid.  When the
+user changes the TUI layout, windows no longer visible in the new
+layout will be destroyed.  At this point, the @code{gdb.TuiWindow}
+will no longer be valid, and methods (and attributes) other than
+@code{is_valid} will throw an exception.
+@end defun
+
+@defvar TuiWindow.width
+This attribute holds the width of the window.  It is not writable.
+@end defvar
+
+@defvar TuiWindow.height
+This attribute holds the height of the window.  It is not writable.
+@end defvar
+
+@defvar TuiWindow.title
+This attribute holds the window's title, a string.  This is normally
+displayed above the window.  This attribute can be modified.
+@end defvar
+
+@defun TuiWindow.erase ()
+Remove all the contents of the window.
+@end defun
+
+@defun TuiWindow.write (@var{string})
+Write @var{string} to the window.  @var{string} can contain ANSI
+terminal escape styling sequences; @value{GDBN} will convert translate
+these as appropriate for the terminal.
+@end defun
+
+The factory function that you supply should return an object
+conforming to the TUI window protocol.  These are the method that can
+be called on this object, which is referred to below as the ``window
+object''.  The methods documented below are optional; if the object
+does not implement one of these methods, @value{GDBN} will not attempt
+to call it.  Additional new methods may be added to the window
+protocol in the future.  @value{GDBN} guarantees that they will begin
+with a lower-case letter, so you can start implementation methods with
+upper-case letters or underscore to avoid any future conflicts.
+
+@defun Window.close ()
+When the TUI window is closed, the @code{gdb.TuiWindow} object will be
+put into an invalid state.  At this time, @value{GDBN} will call
+@code{close} method on the window object.
+
+After this method is called, @value{GDBN} will discard any references
+it holds on this window object, and will no longer call methods on
+this object.
+@end defun
+
+@defun Window.render ()
+In some situations, a TUI window can change size.  For example, this
+can happen if the user resizes the terminal, or changes the layout.
+When this happens, @value{GDBN} will call the @code{render} method on
+the window object.
+
+If your window is intended to update in response to changes in the
+inferior, you will probably also want to register event listeners and
+send output to the @code{gdb.TuiWindow}.
+@end defun
+
+@defun Window.hscroll (@var{num})
+This is a request to scroll the window horizontally.  @var{num} is the
+amount by which to scroll, with negative numbers meaning to scroll
+right.
+@end defun
+
+@defun Window.vscroll (@var{num})
+This is a request to scroll the window vertically.  @var{num} is the
+amount by which to scroll, with negative numbers meaning to scroll
+backward.
+@end defun
+
 @node Python Auto-loading
 @subsection Python Auto-loading
 @cindex Python auto-loading
diff --git a/gdb/python/py-tui.c b/gdb/python/py-tui.c
new file mode 100644
index 00000000000..4cb86ae75da
--- /dev/null
+++ b/gdb/python/py-tui.c
@@ -0,0 +1,510 @@
+/* TUI windows implemented in Python
+
+   Copyright (C) 2020 Free Software Foundation, Inc.
+
+   This file is part of GDB.
+
+   This program is free software; you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+
+#include "defs.h"
+#include "arch-utils.h"
+#include "python-internal.h"
+#include "gdb_curses.h"
+
+#ifdef TUI
+
+#include "tui/tui-data.h"
+#include "tui/tui-io.h"
+#include "tui/tui-layout.h"
+#include "tui/tui-wingeneral.h"
+#include "tui/tui-winsource.h"
+
+class tui_py_window;
+
+/* A PyObject representing a TUI window.  */
+
+struct gdbpy_tui_window
+{
+  PyObject_HEAD
+
+  /* The TUI window, or nullptr if the window has been deleted.  */
+  tui_py_window *window;
+};
+
+extern PyTypeObject gdbpy_tui_window_object_type
+    CPYCHECKER_TYPE_OBJECT_FOR_TYPEDEF ("gdbpy_tui_window");
+
+/* A TUI window written in Python.  */
+
+class tui_py_window : public tui_win_info
+{
+public:
+
+  tui_py_window (const char *name, gdbpy_ref<gdbpy_tui_window> wrapper)
+    : m_name (name),
+      m_wrapper (std::move (wrapper))
+  {
+    m_wrapper->window = this;
+  }
+
+  ~tui_py_window ();
+
+  DISABLE_COPY_AND_ASSIGN (tui_py_window);
+
+  /* Set the "user window" to the indicated reference.  The user
+     window is the object returned the by user-defined window
+     constructor.  */
+  void set_user_window (gdbpy_ref<> &&user_window)
+  {
+    m_window = std::move (user_window);
+  }
+
+  const char *name () const override
+  {
+    return m_name.c_str ();
+  }
+
+  void rerender () override;
+  void do_scroll_vertical (int num_to_scroll) override;
+  void do_scroll_horizontal (int num_to_scroll) override;
+
+  /* Erase and re-box the window.  */
+  void erase ()
+  {
+    if (is_visible ())
+      {
+	werase (handle.get ());
+	check_and_display_highlight_if_needed ();
+	cursor_x = 0;
+	cursor_y = 0;
+      }
+  }
+
+  /* Write STR to the window.  */
+  void output (const char *str);
+
+  /* A helper function to compute the viewport width.  */
+  int viewport_width () const
+  {
+    return std::max (0, width - 2);
+  }
+
+  /* A helper function to compute the viewport height.  */
+  int viewport_height () const
+  {
+    return std::max (0, height - 2);
+  }
+
+private:
+
+  /* Location of the cursor.  */
+  int cursor_x = 0;
+  int cursor_y = 0;
+
+  /* The name of this window.  */
+  std::string m_name;
+
+  /* The underlying Python window object.  */
+  gdbpy_ref<> m_window;
+
+  /* The Python wrapper for this object.  */
+  gdbpy_ref<gdbpy_tui_window> m_wrapper;
+};
+
+tui_py_window::~tui_py_window ()
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  if (PyObject_HasAttrString (m_window.get (), "close"))
+    {
+      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "close",
+					       nullptr));
+      if (result == nullptr)
+	gdbpy_print_stack ();
+    }
+
+  /* Unlink.  */
+  m_wrapper->window = nullptr;
+  /* Explicitly free the Python references.  We have to do this
+     manually because we need to hold the GIL while doing so.  */
+  m_wrapper.reset (nullptr);
+  m_window.reset (nullptr);
+}
+
+void
+tui_py_window::rerender ()
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  if (PyObject_HasAttrString (m_window.get (), "render"))
+    {
+      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "render",
+					       nullptr));
+      if (result == nullptr)
+	gdbpy_print_stack ();
+    }
+}
+
+void
+tui_py_window::do_scroll_horizontal (int num_to_scroll)
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  if (PyObject_HasAttrString (m_window.get (), "hscroll"))
+    {
+      gdbpy_ref<> result (PyObject_CallMethod (m_window.get(), "hscroll",
+					       "i", num_to_scroll, nullptr));
+      if (result == nullptr)
+	gdbpy_print_stack ();
+    }
+}
+
+void
+tui_py_window::do_scroll_vertical (int num_to_scroll)
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  if (PyObject_HasAttrString (m_window.get (), "vscroll"))
+    {
+      gdbpy_ref<> result (PyObject_CallMethod (m_window.get (), "vscroll",
+					       "i", num_to_scroll, nullptr));
+      if (result == nullptr)
+	gdbpy_print_stack ();
+    }
+}
+
+void
+tui_py_window::output (const char *text)
+{
+  int vwidth = viewport_width ();
+
+  while (cursor_y < viewport_height () && *text != '\0')
+    {
+      wmove (handle.get (), cursor_y + 1, cursor_x + 1);
+
+      std::string line = tui_copy_source_line (&text, 0, 0,
+					       vwidth - cursor_x, 0);
+      tui_puts (line.c_str (), handle.get ());
+
+      if (*text == '\n')
+	{
+	  ++text;
+	  ++cursor_y;
+	  cursor_x = 0;
+	}
+      else
+	cursor_x = getcurx (handle.get ()) - 1;
+    }
+
+  wrefresh (handle.get ());
+}
+
+
+
+/* A callable that is used to create a TUI window.  It wraps the
+   user-supplied window constructor.  */
+
+class gdbpy_tui_window_maker
+{
+public:
+
+  explicit gdbpy_tui_window_maker (gdbpy_ref<> &&constr)
+    : m_constr (std::move (constr))
+  {
+  }
+
+  ~gdbpy_tui_window_maker ();
+
+  gdbpy_tui_window_maker (gdbpy_tui_window_maker &&other)
+    : m_constr (std::move (other.m_constr))
+  {
+  }
+
+  gdbpy_tui_window_maker (const gdbpy_tui_window_maker &other)
+  {
+    gdbpy_enter enter_py (get_current_arch (), current_language);
+    m_constr = other.m_constr;
+  }
+
+  gdbpy_tui_window_maker &operator= (gdbpy_tui_window_maker &&other)
+  {
+    m_constr = std::move (other.m_constr);
+    return *this;
+  }
+
+  gdbpy_tui_window_maker &operator= (const gdbpy_tui_window_maker &other)
+  {
+    gdbpy_enter enter_py (get_current_arch (), current_language);
+    m_constr = other.m_constr;
+    return *this;
+  }
+
+  tui_win_info *operator() (const char *name);
+
+private:
+
+  /* A constructor that is called to make a TUI window.  */
+  gdbpy_ref<> m_constr;
+};
+
+gdbpy_tui_window_maker::~gdbpy_tui_window_maker ()
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+  m_constr.reset (nullptr);
+}
+
+tui_win_info *
+gdbpy_tui_window_maker::operator() (const char *win_name)
+{
+  gdbpy_enter enter_py (get_current_arch (), current_language);
+
+  gdbpy_ref<gdbpy_tui_window> wrapper
+    (PyObject_New (gdbpy_tui_window, &gdbpy_tui_window_object_type));
+  if (wrapper == nullptr)
+    {
+      gdbpy_print_stack ();
+      return nullptr;
+    }
+
+  std::unique_ptr<tui_py_window> window
+    (new tui_py_window (win_name, wrapper));
+
+  gdbpy_ref<> user_window
+    (PyObject_CallFunctionObjArgs (m_constr.get (),
+				   (PyObject *) wrapper.get (),
+				   nullptr));
+  if (user_window == nullptr)
+    {
+      gdbpy_print_stack ();
+      return nullptr;
+    }
+
+  window->set_user_window (std::move (user_window));
+  /* Window is now owned by the TUI.  */
+  return window.release ();
+}
+
+/* Implement "gdb.register_window_type".  */
+
+PyObject *
+gdbpy_register_tui_window (PyObject *self, PyObject *args, PyObject *kw)
+{
+  static const char *keywords[] = { "name", "constructor", nullptr };
+
+  const char *name;
+  PyObject *cons_obj;
+
+  if (!gdb_PyArg_ParseTupleAndKeywords (args, kw, "sO", keywords,
+					&name, &cons_obj))
+    return nullptr;
+
+  try
+    {
+      gdbpy_tui_window_maker constr (gdbpy_ref<>::new_reference (cons_obj));
+      tui_register_window (name, constr);
+    }
+  catch (const gdb_exception &except)
+    {
+      gdbpy_convert_exception (except);
+      return nullptr;
+    }
+
+  Py_RETURN_NONE;
+}
+
+
+
+/* Require that "Window" be a valid window.  */
+
+#define REQUIRE_WINDOW(Window)					\
+    do {							\
+      if ((Window)->window == nullptr)				\
+        return PyErr_Format (PyExc_RuntimeError,		\
+                             _("TUI window is invalid."));	\
+    } while (0)
+
+/* Python function which checks the validity of a TUI window
+   object.  */
+static PyObject *
+gdbpy_tui_is_valid (PyObject *self, PyObject *args)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+  if (win->window != nullptr)
+    Py_RETURN_TRUE;
+  Py_RETURN_FALSE;
+}
+
+/* Python function that erases the TUI window.  */
+static PyObject *
+gdbpy_tui_erase (PyObject *self, PyObject *args)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+  REQUIRE_WINDOW (win);
+
+  win->window->erase ();
+
+  Py_RETURN_NONE;
+}
+
+/* Python function that writes some text to a TUI window.  */
+static PyObject *
+gdbpy_tui_write (PyObject *self, PyObject *args)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+  const char *text;
+
+  if (!PyArg_ParseTuple (args, "s", &text))
+    return nullptr;
+
+  REQUIRE_WINDOW (win);
+
+  win->window->output (text);
+
+  Py_RETURN_NONE;
+}
+
+/* Return the width of the TUI window.  */
+static PyObject *
+gdbpy_tui_width (PyObject *self, void *closure)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+  REQUIRE_WINDOW (win);
+  return PyLong_FromLong (win->window->viewport_width ());
+}
+
+/* Return the height of the TUI window.  */
+static PyObject *
+gdbpy_tui_height (PyObject *self, void *closure)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+  REQUIRE_WINDOW (win);
+  return PyLong_FromLong (win->window->viewport_height ());
+}
+
+/* Return the title of the TUI window.  */
+static PyObject *
+gdbpy_tui_title (PyObject *self, void *closure)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+  REQUIRE_WINDOW (win);
+  return host_string_to_python_string (win->window->title.c_str ()).release ();
+}
+
+/* Set the title of the TUI window.  */
+static int
+gdbpy_tui_set_title (PyObject *self, PyObject *newvalue, void *closure)
+{
+  gdbpy_tui_window *win = (gdbpy_tui_window *) self;
+
+  if (win->window == nullptr)
+    {
+      PyErr_Format (PyExc_RuntimeError, _("TUI window is invalid."));
+      return -1;
+    }
+
+  if (win->window == nullptr)
+    {
+      PyErr_Format (PyExc_TypeError, _("Cannot delete \"title\" attribute."));
+      return -1;
+    }
+
+  gdb::unique_xmalloc_ptr<char> value
+    = python_string_to_host_string (newvalue);
+  if (value == nullptr)
+    return -1;
+
+  win->window->title = value.get ();
+  return 0;
+}
+
+static gdb_PyGetSetDef tui_object_getset[] =
+{
+  { "width", gdbpy_tui_width, NULL, "Width of the window.", NULL },
+  { "height", gdbpy_tui_height, NULL, "Height of the window.", NULL },
+  { "title", gdbpy_tui_title, gdbpy_tui_set_title, "Title of the window.",
+    NULL },
+  { NULL }  /* Sentinel */
+};
+
+static PyMethodDef tui_object_methods[] =
+{
+  { "is_valid", gdbpy_tui_is_valid, METH_NOARGS,
+    "is_valid () -> Boolean\n\
+Return true if this TUI window is valid, false if not." },
+  { "erase", gdbpy_tui_erase, METH_NOARGS,
+    "Erase the TUI window." },
+  { "write", (PyCFunction) gdbpy_tui_write, METH_VARARGS,
+    "Append a string to the TUI window." },
+  { NULL } /* Sentinel.  */
+};
+
+PyTypeObject gdbpy_tui_window_object_type =
+{
+  PyVarObject_HEAD_INIT (NULL, 0)
+  "gdb.TuiWindow",		  /*tp_name*/
+  sizeof (gdbpy_tui_window),	  /*tp_basicsize*/
+  0,				  /*tp_itemsize*/
+  0,				  /*tp_dealloc*/
+  0,				  /*tp_print*/
+  0,				  /*tp_getattr*/
+  0,				  /*tp_setattr*/
+  0,				  /*tp_compare*/
+  0,				  /*tp_repr*/
+  0,				  /*tp_as_number*/
+  0,				  /*tp_as_sequence*/
+  0,				  /*tp_as_mapping*/
+  0,				  /*tp_hash */
+  0,				  /*tp_call*/
+  0,				  /*tp_str*/
+  0,				  /*tp_getattro*/
+  0,				  /*tp_setattro */
+  0,				  /*tp_as_buffer*/
+  Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,  /*tp_flags*/
+  "GDB TUI window object",	  /* tp_doc */
+  0,				  /* tp_traverse */
+  0,				  /* tp_clear */
+  0,				  /* tp_richcompare */
+  0,				  /* tp_weaklistoffset */
+  0,				  /* tp_iter */
+  0,				  /* tp_iternext */
+  tui_object_methods,		  /* tp_methods */
+  0,				  /* tp_members */
+  tui_object_getset,		  /* tp_getset */
+  0,				  /* tp_base */
+  0,				  /* tp_dict */
+  0,				  /* tp_descr_get */
+  0,				  /* tp_descr_set */
+  0,				  /* tp_dictoffset */
+  0,				  /* tp_init */
+  0,				  /* tp_alloc */
+};
+
+#endif /* TUI */
+
+/* Initialize this module.  */
+
+int
+gdbpy_initialize_tui ()
+{
+#ifdef TUI
+  gdbpy_tui_window_object_type.tp_new = PyType_GenericNew;
+  if (PyType_Ready (&gdbpy_tui_window_object_type) < 0)
+    return -1;
+#endif	/* TUI */
+
+  return 0;
+}
diff --git a/gdb/python/python-internal.h b/gdb/python/python-internal.h
index e2464548a7e..bbb66bd0f5c 100644
--- a/gdb/python/python-internal.h
+++ b/gdb/python/python-internal.h
@@ -447,6 +447,8 @@ PyObject *gdbpy_parameter_value (enum var_types type, void *var);
 char *gdbpy_parse_command_name (const char *name,
 				struct cmd_list_element ***base_list,
 				struct cmd_list_element **start_list);
+PyObject *gdbpy_register_tui_window (PyObject *self, PyObject *args,
+				     PyObject *kw);
 
 PyObject *symtab_and_line_to_sal_object (struct symtab_and_line sal);
 PyObject *symtab_to_symtab_object (struct symtab *symtab);
@@ -543,6 +545,8 @@ int gdbpy_initialize_xmethods (void)
   CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
 int gdbpy_initialize_unwind (void)
   CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
+int gdbpy_initialize_tui ()
+  CPYCHECKER_NEGATIVE_RESULT_SETS_EXCEPTION;
 
 /* A wrapper for PyErr_Fetch that handles reference counting for the
    caller.  */
diff --git a/gdb/python/python.c b/gdb/python/python.c
index bf214fae6e2..55103de2bd1 100644
--- a/gdb/python/python.c
+++ b/gdb/python/python.c
@@ -1686,7 +1686,8 @@ do_start_initialization ()
       || gdbpy_initialize_event () < 0
       || gdbpy_initialize_arch () < 0
       || gdbpy_initialize_xmethods () < 0
-      || gdbpy_initialize_unwind () < 0)
+      || gdbpy_initialize_unwind () < 0
+      || gdbpy_initialize_tui () < 0)
     return false;
 
 #define GDB_PY_DEFINE_EVENT_TYPE(name, py_name, doc, base)	\
@@ -2038,6 +2039,13 @@ or None if not set." },
     "convenience_variable (NAME, VALUE) -> None.\n\
 Set the value of the convenience variable $NAME." },
 
+#ifdef TUI
+  { "register_window_type", (PyCFunction) gdbpy_register_tui_window,
+    METH_VARARGS | METH_KEYWORDS,
+    "register_window_type (NAME, CONSTRUCSTOR) -> None\n\
+Register a TUI window constructor." },
+#endif	/* TUI */
+
   {NULL, NULL, 0, NULL}
 };
 
diff --git a/gdb/testsuite/gdb.python/tui-window.exp b/gdb/testsuite/gdb.python/tui-window.exp
new file mode 100644
index 00000000000..5c8909d4079
--- /dev/null
+++ b/gdb/testsuite/gdb.python/tui-window.exp
@@ -0,0 +1,51 @@
+# Copyright (C) 2020 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# Test a TUI window implemented in Python.
+
+load_lib gdb-python.exp
+load_lib tuiterm.exp
+
+# This test doesn't care about the inferior.
+standard_testfile py-arch.c
+
+if {[build_executable "failed to prepare" ${testfile} ${srcfile}] == -1} {
+    return -1
+}
+
+Term::clean_restart 24 80 $testfile
+
+# Skip all tests if Python scripting is not enabled.
+if { [skip_python_tests] } { continue }
+
+set remote_python_file [gdb_remote_download host \
+			    ${srcdir}/${subdir}/${testfile}.py]
+gdb_test_no_output "source ${remote_python_file}" \
+    "source ${testfile}.py"
+
+gdb_test_no_output "tui new-layout test test 1 locator 0 cmd 1"
+
+if {![Term::enter_tui]} {
+    unsupported "TUI not supported"
+}
+
+Term::command "layout test"
+Term::check_contents "test title" \
+    "This Is The Title"
+Term::check_contents "Window display" "Test: 0"
+
+Term::resize 51 51
+# Remember that a resize request actually does two resizes...
+Term::check_contents "Window was updated" "Test: 2"
diff --git a/gdb/testsuite/gdb.python/tui-window.py b/gdb/testsuite/gdb.python/tui-window.py
new file mode 100644
index 00000000000..4deb585f138
--- /dev/null
+++ b/gdb/testsuite/gdb.python/tui-window.py
@@ -0,0 +1,37 @@
+# Copyright (C) 2020 Free Software Foundation, Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# A TUI window implemented in Python.
+
+import gdb
+
+the_window = None
+
+class TestWindow:
+    def __init__(self, win):
+        global the_window
+        the_window = win
+        self.count = 0
+        self.win = win
+        win.title = "This Is The Title"
+
+    def render(self):
+        self.win.erase()
+        w = self.win.width
+        h = self.win.height
+        self.win.write("Test: " + str(self.count) + " " + str(w) + "x" + str(h))
+        self.count = self.count + 1
+
+gdb.register_window_type("test", TestWindow)
diff --git a/gdb/tui/tui-layout.c b/gdb/tui/tui-layout.c
index 797acc6e8b7..d55660f89f6 100644
--- a/gdb/tui/tui-layout.c
+++ b/gdb/tui/tui-layout.c
@@ -65,6 +65,11 @@ static tui_layout_split *asm_regs_layout;
 /* See tui-data.h.  */
 std::vector<tui_win_info *> tui_windows;
 
+/* When applying a layout, this is the list of all windows that were
+   in the previous layout.  This is used to re-use windows when
+   changing a layout.  */
+static std::vector<tui_win_info *> saved_tui_windows;
+
 /* See tui-layout.h.  */
 
 void
@@ -75,10 +80,10 @@ tui_apply_current_layout ()
 
   extract_display_start_addr (&gdbarch, &addr);
 
-  std::vector<tui_win_info *> saved_windows = std::move (tui_windows);
+  saved_tui_windows = std::move (tui_windows);
   tui_windows.clear ();
 
-  for (tui_win_info *win_info : saved_windows)
+  for (tui_win_info *win_info : saved_tui_windows)
     win_info->make_visible (false);
 
   applied_layout->apply (0, 0, tui_term_width (), tui_term_height ());
@@ -94,7 +99,7 @@ tui_apply_current_layout ()
 
   /* Now delete any window that was not re-applied.  */
   tui_win_info *focus = tui_win_with_focus ();
-  for (tui_win_info *win_info : saved_windows)
+  for (tui_win_info *win_info : saved_tui_windows)
     {
       if (!win_info->is_visible ())
 	{
@@ -107,6 +112,8 @@ tui_apply_current_layout ()
   if (gdbarch == nullptr && TUI_DISASM_WIN != nullptr)
     tui_get_begin_asm_address (&gdbarch, &addr);
   tui_update_source_windows_with_addr (gdbarch, addr);
+
+  saved_tui_windows.clear ();
 }
 
 /* See tui-layout.  */
@@ -395,6 +402,21 @@ initialize_known_windows ()
 
 /* See tui-layout.h.  */
 
+void
+tui_register_window (const char *name, window_factory &&factory)
+{
+  std::string name_copy = name;
+
+  if (name_copy == "src" || name_copy == "cmd" || name_copy == "regs"
+      || name_copy == "asm" || name_copy == "locator")
+    error (_("Window type \"%s\" is built-in"), name);
+
+  known_window_types->emplace (std::move (name_copy),
+			       std::move (factory));
+}
+
+/* See tui-layout.h.  */
+
 std::unique_ptr<tui_layout_base>
 tui_layout_window::clone () const
 {
diff --git a/gdb/tui/tui-layout.h b/gdb/tui/tui-layout.h
index 6607e8d40d8..90618377e17 100644
--- a/gdb/tui/tui-layout.h
+++ b/gdb/tui/tui-layout.h
@@ -249,4 +249,14 @@ extern void tui_apply_current_layout ();
 extern void tui_adjust_window_height (struct tui_win_info *win,
 				      int new_height);
 
+/* The type of a function that is used to create a TUI window.  */
+
+typedef std::function<tui_gen_win_info * (const char *name)> window_factory;
+
+/* Register a new TUI window type.  NAME is the name of the window
+   type.  FACTORY is a function that can be called to instantiate the
+   window.  */
+
+extern void tui_register_window (const char *name, window_factory &&factory);
+
 #endif /* TUI_TUI_LAYOUT_H */
