diff --git a/gdb/NEWS b/gdb/NEWS
index f51a989fef1..b8939693f4e 100644
--- a/gdb/NEWS
+++ b/gdb/NEWS
@@ -7,6 +7,12 @@
   that support it (see entry for GDB 9, below), providing faster
   performance for programs with many symbols.
 
+* New commands
+
+tui new-layout NAME WINDOW WEIGHT [WINDOW WEIGHT]...
+  Define a new TUI layout, specifying its name and the windows that
+  will be displayed.
+
 *** Changes in GDB 9
 
 * 'thread-exited' event is now available in the annotations interface.
diff --git a/gdb/doc/gdb.texinfo b/gdb/doc/gdb.texinfo
index 65137b17396..147d0c28c92 100644
--- a/gdb/doc/gdb.texinfo
+++ b/gdb/doc/gdb.texinfo
@@ -27666,6 +27666,8 @@ source and registers, or
 assembly and registers.
 @end itemize
 
+These are the standard layouts, but other layouts can be defined.
+
 A status line above the command window shows the following information:
 
 @table @emph
@@ -27902,11 +27904,40 @@ Disable TUI mode, returning to the console interpreter.
 @kindex info win
 List and give the size of all displayed windows.
 
+@item tui new-layout @var{name} @var{window} @var{weight} @r{[}@var{window} @var{weight}@dots{}@r{]}
+@kindex tui new-layout
+Create a new TUI layout.  The new layout will be named @var{name}, and
+can be accessed using the @code{layout} command (see below).
+
+Each @var{window} parameter is the name of a window to display.  The
+windows will be displayed from top to bottom in the order listed.  The
+names of the windows are the same as the ones given to the
+@code{focus} command (see below); additional, the @code{locator}
+window can be specified.
+
+Each @var{weight} is an integer.  It is the weight of this window
+relative to all the other windows in the layout.  These numbers are
+used to calculate how much of the screen is given to each window.
+
+For example:
+
+@example
+(gdb) tui new-layout example src 1 regs 1 locator 0 cmd 1
+@end example
+
+Here, the new layout is called @samp{example}.  It shows the source
+and register windows, followed by the locator, and then finally the
+command window.  The non-locator windows all have the same weight, so
+the terminal will be split into three roughly equal sections.
+
 @item layout @var{name}
 @kindex layout
-Changes which TUI windows are displayed.  In each layout the command
-window is always displayed, the @var{name} parameter controls which
-additional windows are displayed, and can be any of the following:
+Changes which TUI windows are displayed.  The @var{name} parameter
+controls which layout is shown.  It can be either one of the built-in
+layout names, or the name of a layout defined by the user using
+@code{tui new-layout}.
+
+The built-in layouts are as follows:
 
 @table @code
 @item next
diff --git a/gdb/testsuite/gdb.tui/new-layout.exp b/gdb/testsuite/gdb.tui/new-layout.exp
new file mode 100644
index 00000000000..8475a9db295
--- /dev/null
+++ b/gdb/testsuite/gdb.tui/new-layout.exp
@@ -0,0 +1,54 @@
+# Copyright 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 "tui new-layout".
+
+load_lib "tuiterm.exp"
+
+standard_testfile tui-layout.c
+
+if {[build_executable "failed to prepare" ${testfile} ${srcfile}] == -1} {
+    return -1
+}
+
+Term::clean_restart 24 80 $testfile
+
+gdb_test "tui new-layout" \
+    "No layout name specified"
+gdb_test "tui new-layout example" \
+    "New layout does not contain any windows"
+gdb_test "tui new-layout example zzq" \
+    "Unknown window \"zzq\""
+gdb_test "tui new-layout example src 1 src 1" \
+    "Window \"src\" seen twice in layout"
+gdb_test "tui new-layout example src 1" \
+    "New layout does not contain the \"cmd\" window"
+
+gdb_test_no_output "tui new-layout example asm 1 locator 0 cmd 1"
+
+gdb_test "help layout example" \
+    "Apply the \"example\" layout.*tui new-layout example asm 1 locator 0 cmd 1"
+
+if {![Term::enter_tui]} {
+    unsupported "TUI not supported"
+}
+
+set text [Term::get_all_lines]
+gdb_assert {![string match "No Source Available" $text]} \
+    "initial source listing"
+
+Term::command "layout example"
+Term::check_contents "example layout shows assembly" \
+    "No Assembly Available"
diff --git a/gdb/tui/tui-layout.c b/gdb/tui/tui-layout.c
index 72a8d27df40..438a3a03828 100644
--- a/gdb/tui/tui-layout.c
+++ b/gdb/tui/tui-layout.c
@@ -27,7 +27,9 @@
 #include "source.h"
 #include "cli/cli-cmds.h"
 #include "cli/cli-decode.h"
+#include "cli/cli-utils.h"
 #include <ctype.h>
+#include <unordered_set>
 
 #include "tui/tui.h"
 #include "tui/tui-command.h"
@@ -397,6 +399,14 @@ tui_layout_window::replace_window (const char *name, const char *new_window)
 
 /* See tui-layout.h.  */
 
+void
+tui_layout_window::specification (ui_file *output)
+{
+  fputs_unfiltered (get_name (), output);
+}
+
+/* See tui-layout.h.  */
+
 tui_layout_split *
 tui_layout_split::add_split (int weight)
 {
@@ -698,6 +708,22 @@ tui_layout_split::replace_window (const char *name, const char *new_window)
     item.layout->replace_window (name, new_window);
 }
 
+/* See tui-layout.h.  */
+
+void
+tui_layout_split::specification (ui_file *output)
+{
+  bool first = true;
+  for (auto &item : m_splits)
+    {
+      if (!first)
+	fputs_unfiltered (" ", output);
+      first = false;
+      item.layout->specification (output);
+      fprintf_unfiltered (output, " %d", item.weight);
+    }
+}
+
 /* Destroy the layout associated with SELF.  */
 
 static void
@@ -714,13 +740,19 @@ static struct cmd_list_element *layout_list;
 
 /* Add a "layout" command with name NAME that switches to LAYOUT.  */
 
-static void
+static struct cmd_list_element *
 add_layout_command (const char *name, tui_layout_split *layout)
 {
   struct cmd_list_element *cmd;
 
-  gdb::unique_xmalloc_ptr<char> doc (xstrprintf (_("Apply the \"%s\" layout"),
-						 name));
+  string_file spec;
+  layout->specification (&spec);
+
+  gdb::unique_xmalloc_ptr<char> doc
+    (xstrprintf (_("Apply the \"%s\" layout.\n\
+This layout was created using:\n\
+  tui new-layout %s %s"),
+		 name, name, spec.c_str ()));
 
   cmd = add_cmd (name, class_tui, nullptr, doc.get (), &layout_list);
   set_cmd_context (cmd, layout);
@@ -730,6 +762,8 @@ add_layout_command (const char *name, tui_layout_split *layout)
   cmd->doc_allocated = 1;
   doc.release ();
   layouts.emplace_back (layout);
+
+  return cmd;
 }
 
 /* Initialize the standard layouts.  */
@@ -777,6 +811,59 @@ initialize_layouts ()
 
 
 
+/* A helper function that returns true if NAME is the name of an
+   available window.  */
+
+static bool
+validate_window_name (const std::string &name)
+{
+  return (name == "src" || name == "cmd"
+	  || name == "regs" || name == "asm"
+	  || name == "locator");
+}
+
+/* Implementation of the "tui new-layout" command.  */
+
+static void
+tui_new_layout_command (const char *spec, int from_tty)
+{
+  std::string new_name = extract_arg (&spec);
+  if (new_name.empty ())
+    error (_("No layout name specified"));
+  if (new_name[0] == '-')
+    error (_("Layout name cannot start with '-'"));
+
+  std::unique_ptr<tui_layout_split> new_layout (new tui_layout_split);
+  std::unordered_set<std::string> seen_windows;
+  while (true)
+    {
+      std::string name = extract_arg (&spec);
+      if (name.empty ())
+	break;
+      if (!validate_window_name (name))
+	error (_("Unknown window \"%s\""), name.c_str ());
+      if (seen_windows.find (name) != seen_windows.end ())
+	error (_("Window \"%s\" seen twice in layout"), name.c_str ());
+      ULONGEST weight = get_ulongest (&spec);
+      if ((int) weight != weight)
+	error (_("Weight out of range: %s"), pulongest (weight));
+      new_layout->add_window (name.c_str (), weight);
+      seen_windows.insert (name);
+    }
+  if (seen_windows.empty ())
+    error (_("New layout does not contain any windows"));
+  if (seen_windows.find ("cmd") == seen_windows.end ())
+    error (_("New layout does not contain the \"cmd\" window"));
+
+  gdb::unique_xmalloc_ptr<char> cmd_name
+    = make_unique_xstrdup (new_name.c_str ());
+  struct cmd_list_element *cmd
+    = add_layout_command (cmd_name.get (), new_layout.get ());
+  cmd->name_allocated = 1;
+  cmd_name.release ();
+  new_layout.release ();
+}
+
 /* Base command for "layout".  */
 
 static void
@@ -806,5 +893,15 @@ Usage: layout prev | next | LAYOUT-NAME"),
 	   _("Apply the TUI register layout"),
 	   &layout_list);
 
+  add_cmd ("new-layout", class_tui, tui_new_layout_command,
+	   _("Create a new TUI layout.\n\
+Usage: tui new-layout NAME WINDOW WEIGHT [WINDOW WEIGHT]...\n\
+Create a new TUI layout.  The new layout will be named NAME,\n\
+and can be accessed using \"layout NAME\".\n\
+The windows will be displayed in the specified order.\n\
+Each WEIGHT is an integer, which holds the relative size\n\
+to be allocated to the window."),
+	   tui_get_cmd_list ());
+
   initialize_layouts ();
 }
diff --git a/gdb/tui/tui-layout.h b/gdb/tui/tui-layout.h
index 98bd548aaee..c2249a783f8 100644
--- a/gdb/tui/tui-layout.h
+++ b/gdb/tui/tui-layout.h
@@ -22,6 +22,8 @@
 #ifndef TUI_TUI_LAYOUT_H
 #define TUI_TUI_LAYOUT_H
 
+#include "ui-file.h"
+
 #include "tui/tui.h"
 #include "tui/tui-data.h"
 
@@ -72,6 +74,9 @@ public:
      NEW_WINDOW.  */
   virtual void replace_window (const char *name, const char *new_window) = 0;
 
+  /* Append the specification to this window to OUTPUT.  */
+  virtual void specification (ui_file *output) = 0;
+
   /* The most recent space allocation.  */
   int x = 0;
   int y = 0;
@@ -120,6 +125,8 @@ public:
 
   void replace_window (const char *name, const char *new_window) override;
 
+  void specification (ui_file *output) override;
+
 protected:
 
   void get_sizes (int *min_height, int *max_height) override;
@@ -167,6 +174,8 @@ public:
 
   void replace_window (const char *name, const char *new_window) override;
 
+  void specification (ui_file *output) override;
+
 protected:
 
   void get_sizes (int *min_height, int *max_height) override;
