[20/24] Allow TUI windows in Python

Message ID 20200104183410.17114-21-tom@tromey.com
State New, archived
Headers

Commit Message

Tom Tromey Jan. 4, 2020, 6:34 p.m. UTC
  This patch adds support for writing new TUI windows in Python.

2020-01-04  Tom Tromey  <tom@tromey.com>

	* NEWS: Add entry for gdb.register_window_type.
	* tui/tui-layout.h (window_factory): New typedef.
	(tui_register_window): Declare.
	* tui/tui-layout.c (saved_tui_windows): New global.
	(tui_apply_current_layout): Use it.
	(tui_register_window): New function.
	* python/python.c (do_start_initialization): Call
	gdbpy_initialize_tui.
	(python_GdbMethods): Add "register_window_type" function.
	* python/python-internal.h (gdbpy_register_tui_window)
	(gdbpy_initialize_tui): Declare.
	* python/py-tui.c: New file.
	* Makefile.in (SUBDIR_PYTHON_SRCS): Add py-tui.c.

gdb/doc/ChangeLog
2020-01-04  Tom Tromey  <tom@tromey.com>

	* python.texi (Python API): Add menu item.
	(TUI Windows In Python): New node.

gdb/testsuite/ChangeLog
2020-01-04  Tom Tromey  <tom@tromey.com>

	* gdb.python/tui-window.exp: New file.
	* gdb.python/tui-window.py: New file.

Change-Id: I85fbfb923a1840450a00a7dce113a05d7f048baa
---
 gdb/ChangeLog                           |  16 +
 gdb/Makefile.in                         |   1 +
 gdb/NEWS                                |   5 +
 gdb/doc/ChangeLog                       |   5 +
 gdb/doc/python.texi                     | 101 +++++
 gdb/python/py-tui.c                     | 510 ++++++++++++++++++++++++
 gdb/python/python-internal.h            |   4 +
 gdb/python/python.c                     |  10 +-
 gdb/testsuite/ChangeLog                 |   5 +
 gdb/testsuite/gdb.python/tui-window.exp |  51 +++
 gdb/testsuite/gdb.python/tui-window.py  |  37 ++
 gdb/tui/tui-layout.c                    |  28 +-
 gdb/tui/tui-layout.h                    |  10 +
 13 files changed, 779 insertions(+), 4 deletions(-)
 create mode 100644 gdb/python/py-tui.c
 create mode 100644 gdb/testsuite/gdb.python/tui-window.exp
 create mode 100644 gdb/testsuite/gdb.python/tui-window.py
  

Comments

Eli Zaretskii Jan. 4, 2020, 6:57 p.m. UTC | #1
> From: Tom Tromey <tom@tromey.com>
> Cc: Tom Tromey <tom@tromey.com>
> Date: Sat,  4 Jan 2020 11:34:06 -0700
> 
> 2020-01-04  Tom Tromey  <tom@tromey.com>
> 
> 	* NEWS: Add entry for gdb.register_window_type.
> 	* tui/tui-layout.h (window_factory): New typedef.
> 	(tui_register_window): Declare.
> 	* tui/tui-layout.c (saved_tui_windows): New global.
> 	(tui_apply_current_layout): Use it.
> 	(tui_register_window): New function.
> 	* python/python.c (do_start_initialization): Call
> 	gdbpy_initialize_tui.
> 	(python_GdbMethods): Add "register_window_type" function.
> 	* python/python-internal.h (gdbpy_register_tui_window)
> 	(gdbpy_initialize_tui): Declare.
> 	* python/py-tui.c: New file.
> 	* Makefile.in (SUBDIR_PYTHON_SRCS): Add py-tui.c.
> 
> gdb/doc/ChangeLog
> 2020-01-04  Tom Tromey  <tom@tromey.com>
> 
> 	* python.texi (Python API): Add menu item.
> 	(TUI Windows In Python): New node.
> 
> gdb/testsuite/ChangeLog
> 2020-01-04  Tom Tromey  <tom@tromey.com>
> 
> 	* gdb.python/tui-window.exp: New file.
> 	* gdb.python/tui-window.py: New file.

OK for the documentation parts, with two comments.

> +terminal escape styling sequences; @value{GDBN} will convert translate
> +these as appropriate for the terminal.               ^^^^^^^^^^^^^^^^^

One of these two words should be removed.

> +@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.

There's a well-known source of confusion with describing scrolling
direction: does it refer to scrolling of the text in the window
(i.e. the window is considered to be fixed and the text to be
scrolled) or the other way around?  The confusion stems from the fact
that we say "scroll the window", but what is actually scrolled is the
text.  Can we please make it crystal clear here what will move right
and backward here?

Thanks.
  
Tom Tromey Feb. 22, 2020, 7:57 p.m. UTC | #2
>>>>> "Eli" == Eli Zaretskii <eliz@gnu.org> writes:

>> +terminal escape styling sequences; @value{GDBN} will convert translate
>> +these as appropriate for the terminal.               ^^^^^^^^^^^^^^^^^

Eli> One of these two words should be removed.

Fixed.

>> +@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.

Eli> There's a well-known source of confusion with describing scrolling
Eli> direction: does it refer to scrolling of the text in the window
Eli> (i.e. the window is considered to be fixed and the text to be
Eli> scrolled) or the other way around?  The confusion stems from the fact
Eli> that we say "scroll the window", but what is actually scrolled is the
Eli> text.  Can we please make it crystal clear here what will move right
Eli> and backward here?

I did this, like so:

@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.  In the TUI model, it is the viewport that moves, not the
contents.  A positive argument should cause the viewport to move
right, and so the content should appear to move to the left.
@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.  In the TUI model, it is the viewport that moves, not the
contents.  A positive argument should cause the viewport to move down,
and so the content should appear to move up.
@end defun



Tom
  
Eli Zaretskii Feb. 22, 2020, 8:18 p.m. UTC | #3
> From: Tom Tromey <tom@tromey.com>
> Cc: Tom Tromey <tom@tromey.com>,  gdb-patches@sourceware.org
> Date: Sat, 22 Feb 2020 12:57:51 -0700
> 
> Eli> There's a well-known source of confusion with describing scrolling
> Eli> direction: does it refer to scrolling of the text in the window
> Eli> (i.e. the window is considered to be fixed and the text to be
> Eli> scrolled) or the other way around?  The confusion stems from the fact
> Eli> that we say "scroll the window", but what is actually scrolled is the
> Eli> text.  Can we please make it crystal clear here what will move right
> Eli> and backward here?
> 
> I did this, like so:

Thanks, this is perfect.
  

Patch

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 */