pw_console: Console user guide

- Add the Pigweed Console user guide
- Help window split into 3 separate windows
  - Upstream rst formatted user guide
  - Downstream project specific help, if provided to embed()
  - Auto-generated keybind list from all active window panes

Testing: Test Help Windows Steps 1-6
Change-Id: I25edf66ac73ba55c60a46d860623d0f2a464a839
Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/54320
Reviewed-by: Keir Mierle <keir@google.com>
Commit-Queue: Anthony DiGirolamo <tonymd@google.com>
diff --git a/pw_console/BUILD.gn b/pw_console/BUILD.gn
index fba633e..bf0b678 100644
--- a/pw_console/BUILD.gn
+++ b/pw_console/BUILD.gn
@@ -21,6 +21,7 @@
     "docs.rst",
     "embedding.rst",
     "internals.rst",
+    "py/pw_console/docs/user_guide.rst",
     "testing.rst",
   ]
 }
diff --git a/pw_console/docs.rst b/pw_console/docs.rst
index 65c5558..446f1e0 100644
--- a/pw_console/docs.rst
+++ b/pw_console/docs.rst
@@ -8,23 +8,15 @@
 `ptpython`_ and a log message viewer in a single-window terminal based
 interface. It is designed to be a replacement for `IPython's embed()`_ function.
 
-.. warning::
-   The Pigweed Console is under heavy development. A user manual and usage
-   information will be documented as features near completion.
-
-Goals
-=====
-
-``pw_console`` is a complete solution for interacting with hardware devices
-using :ref:`module-pw_rpc` over a :ref:`module-pw_hdlc` transport.
-
-The repl allows interactive RPC sending while the log viewer provides immediate
-feedback on device status.
-
 Features
-^^^^^^^^
+========
 
-- Interactive Python repl and log viewer in a single terminal window.
+``pw_console`` aims to be a complete solution for interacting with hardware
+devices using :ref:`module-pw_rpc` over a :ref:`module-pw_hdlc` transport.
+
+- Interactive Python repl and log viewer in a single terminal window. This
+  provides interactive RPC sending while the log viewer provides immediate
+  feedback on device status.
 
 - Easily embeddable within a project's own custom console. This should allow
   users to define their own transport layer.
@@ -46,6 +38,7 @@
 .. toctree::
   :maxdepth: 1
 
+  py/pw_console/docs/user_guide
   embedding
   testing
   internals
diff --git a/pw_console/py/pw_console/__main__.py b/pw_console/py/pw_console/__main__.py
index 049abf7..8629e71 100644
--- a/pw_console/py/pw_console/__main__.py
+++ b/pw_console/py/pw_console/__main__.py
@@ -14,6 +14,7 @@
 """Pigweed Console - Warning: This is a work in progress."""
 
 import argparse
+import inspect
 import logging
 import sys
 import tempfile
@@ -105,9 +106,27 @@
         # Give access to adding log messages from the repl via: `LOG.warning()`
         global_vars = dict(LOG=default_loggers[0])
 
-    pw_console.console_app.embed(global_vars=global_vars,
-                                 loggers=default_loggers,
-                                 test_mode=args.test_mode)
+    help_text = None
+    app_title = None
+    if args.test_mode:
+        app_title = 'Console Test Mode'
+        help_text = inspect.cleandoc("""
+            Welcome to the Pigweed Console Test Mode!
+
+            Example commands:
+
+              rpcs.pw.rpc.EchoService.Echo(msg='hello!')
+
+              LOG.warning('Message appears console log window.')
+        """)
+
+    pw_console.console_app.embed(
+        global_vars=global_vars,
+        loggers=default_loggers,
+        test_mode=args.test_mode,
+        help_text=help_text,
+        app_title=app_title,
+    )
 
     if args.logfile:
         print(f'Logs saved to: {args.logfile}')
diff --git a/pw_console/py/pw_console/console_app.py b/pw_console/py/pw_console/console_app.py
index 081efcb..38e036e 100644
--- a/pw_console/py/pw_console/console_app.py
+++ b/pw_console/py/pw_console/console_app.py
@@ -152,13 +152,21 @@
                 'Ctrl-W', 'Quit '))
 
         # Top level UI state toggles.
-        self.show_help_window = False
         self.vertical_split = False
         self.load_theme()
 
-        self.help_window = HelpWindow(self,
-                                      preamble='Pigweed CLI v0.1',
-                                      additional_help_text=help_text)
+        # Pigweed upstream RST user guide
+        self.user_guide_window = HelpWindow(self)
+        self.user_guide_window.load_user_guide()
+
+        # Auto-generated keybindings list for all active panes
+        self.keybind_help_window = HelpWindow(self)
+
+        # Downstream project specific help text
+        self.app_help_text = help_text if help_text else None
+        self.app_help_window = HelpWindow(self, additional_help_text=help_text)
+        self.app_help_window.generate_help_text()
+
         # Used for tracking which pane was in focus before showing help window.
         self.last_focused_pane = None
 
@@ -206,13 +214,27 @@
                     right=0,
                     height=1,
                 ),
-                # Centered floating Help Window
+                # Centered floating help windows
                 Float(
-                    content=self.help_window,
+                    content=self.app_help_window,
                     top=2,
                     bottom=2,
                     # Callable to get width
-                    width=self.help_window.content_width,
+                    width=self.app_help_window.content_width,
+                ),
+                Float(
+                    content=self.user_guide_window,
+                    top=2,
+                    bottom=2,
+                    # Callable to get width
+                    width=self.user_guide_window.content_width,
+                ),
+                Float(
+                    content=self.keybind_help_window,
+                    top=2,
+                    bottom=2,
+                    # Callable to get width
+                    width=self.keybind_help_window.content_width,
                 ),
                 # Completion menu that can overlap other panes since it lives in
                 # the top level Float container.
@@ -382,13 +404,25 @@
             )
         ]
 
+        help_menu_items = [
+            MenuItem('User Guide',
+                     handler=self.user_guide_window.toggle_display),
+            MenuItem('Keyboard Shortcuts',
+                     handler=self.keybind_help_window.toggle_display),
+        ]
+
+        if self.app_help_text:
+            help_menu_items.extend([
+                MenuItem('-'),
+                MenuItem(self.app_title + ' Help',
+                         handler=self.app_help_window.toggle_display)
+            ])
+
         help_menu = [
             # Info / Help
             MenuItem(
                 '[Help]',
-                children=[
-                    MenuItem('Keyboard Shortcuts', handler=self.toggle_help),
-                ],
+                children=help_menu_items,
             ),
         ]
 
@@ -627,24 +661,25 @@
             'Scroll current window.': ['Scroll wheel'],
         }
 
-        self.help_window.add_custom_keybinds_help_text('Global Mouse',
-                                                       mouse_functions)
+        self.keybind_help_window.add_custom_keybinds_help_text(
+            'Global Mouse', mouse_functions)
 
         # Add global key bindings to the help text.
-        self.help_window.add_keybind_help_text('Global', self.key_bindings)
+        self.keybind_help_window.add_keybind_help_text('Global',
+                                                       self.key_bindings)
 
         # Add activated plugin key bindings to the help text.
         for pane in self.active_panes:
             for key_bindings in pane.get_all_key_bindings():
                 help_section_title = pane.__class__.__name__
                 if isinstance(key_bindings, KeyBindings):
-                    self.help_window.add_keybind_help_text(
+                    self.keybind_help_window.add_keybind_help_text(
                         help_section_title, key_bindings)
                 elif isinstance(key_bindings, dict):
-                    self.help_window.add_custom_keybinds_help_text(
+                    self.keybind_help_window.add_custom_keybinds_help_text(
                         help_section_title, key_bindings)
 
-        self.help_window.generate_help_text()
+        self.keybind_help_window.generate_help_text()
 
     def _update_split_orientation(self):
         if self.vertical_split:
@@ -690,20 +725,13 @@
         """Return the currently focused window."""
         return self.application.layout.current_window
 
-    def toggle_help(self):
-        """Toggle visibility of the help window."""
-        # Toggle state variable.
-        self.show_help_window = not self.show_help_window
-
-        # Set the help window in focus.
-        if self.show_help_window:
-            self.last_focused_pane = self.focused_window()
-            self.application.layout.focus(self.help_window)
-        # Restore original focus.
-        else:
-            if self.last_focused_pane:
-                self.application.layout.focus(self.last_focused_pane)
-            self.last_focused_pane = None
+    def modal_window_is_open(self):
+        if self.app_help_text:
+            return (self.app_help_window.show_window
+                    or self.keybind_help_window.show_window
+                    or self.user_guide_window.show_window)
+        return (self.keybind_help_window.show_window
+                or self.user_guide_window.show_window)
 
     def exit_console(self):
         """Quit the console prompt_toolkit application UI."""
diff --git a/pw_console/py/pw_console/docs/user_guide.rst b/pw_console/py/pw_console/docs/user_guide.rst
new file mode 100644
index 0000000..6b07451
--- /dev/null
+++ b/pw_console/py/pw_console/docs/user_guide.rst
@@ -0,0 +1,465 @@
+.. _module-pw_console-user_guide:
+
+User Guide
+==========
+
+.. seealso::
+
+   This guide can be viewed online at: https://pigweed.dev/pw_console/
+
+The Pigweed Console provides a Python repl (read eval print loop) and log viewer
+in a single-window terminal based interface.
+
+.. contents::
+   :local:
+
+
+Starting the Console
+--------------------
+
+::
+
+  pw rpc -s localhost:33000 --proto-globs pw_rpc/echo.proto
+
+
+Exiting
+~~~~~~~
+
+1.  Click the :guilabel:`[File]` menu and then :guilabel:`Exit`.
+2.  Type :guilabel:`quit` or :guilabel:`exit` in the Python Input window.
+3.  The keyboard shortcut :guilabel:`Ctrl-W` also quits.
+
+
+Interface Layout
+----------------
+
+On startup the console will display multiple windows one on top of the other.
+
+::
+
+  +-----------------------------------------------------+
+  | [File] [View] [Window] [Help]       Pigweed Console |
+  +=====================================================+
+  |                                                     |
+  |                                                     |
+  |                                                     |
+  | Log Window                                          |
+  +=====================================================+
+  |                                                     |
+  |                                                     |
+  | Python Results                                      |
+  +-----------------------------------------------------+
+  |                                                     |
+  | Python Input                                        |
+  +-----------------------------------------------------+
+
+
+Navigation
+----------
+
+All menus, windows, and toolbar buttons can be clicked on. Scrolling with the
+mouse wheel should work too. This requires that your terminal is able to send
+mouse events.
+
+
+Main Menu Navigation with the Keyboard
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Move focus between all active UI elements     :guilabel:`Shift-Tab`
+
+Move focus between windows and the main menu  :guilabel:`Ctrl-Up`
+                                              :guilabel:`Ctrl-Down`
+
+Move selection in the main menu               :guilabel:`Up`
+                                              :guilabel:`Down`
+                                              :guilabel:`Left`
+                                              :guilabel:`Right`
+============================================  =====================
+
+
+Toolbars
+~~~~~~~~
+
+Log toolbar functions are clickable. You can also press the keyboard
+shortcut highlighted in blue:
+
+::
+
+        / → Search  f → [✓] Follow  t → [✓] Table  w → [ ] Wrap  C → Clear
+
+
+Log Window
+~~~~~~~~~~
+
+Log Window Scrolling
+^^^^^^^^^^^^^^^^^^^^
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Scroll logs up                                :guilabel:`Mouse Wheel Up`
+                                              :guilabel:`Up`
+                                              :guilabel:`k`
+
+Scroll logs down                              :guilabel:`Mouse Wheel Down`
+                                              :guilabel:`Down`
+                                              :guilabel:`j`
+
+Scroll logs up one page                       :guilabel:`PageUp`
+Scroll logs down one page                     :guilabel:`PageDown`
+Jump to the beginning                         :guilabel:`g`
+Jump to the end                               :guilabel:`G`
+
+Horizontal scroll left or right               :guilabel:`Left`
+                                              :guilabel:`Right`
+
+Horizontal scroll to the beginning            :guilabel:`Home`
+                                              :guilabel:`0`
+                                              :guilabel:`^`
+============================================  =====================
+
+Log Window View Options
+^^^^^^^^^^^^^^^^^^^^^^^
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Toggle line following.                        :guilabel:`f`
+Toggle table view.                            :guilabel:`t`
+Toggle line wrapping.                         :guilabel:`w`
+Clear log pane history.                       :guilabel:`C`
+============================================  =====================
+
+Log Window Management
+^^^^^^^^^^^^^^^^^^^^^^^
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Duplicate this log pane.                      :guilabel:`Insert`
+Remove log pane.                              :guilabel:`Delete`
+============================================  =====================
+
+Log Searching
+^^^^^^^^^^^^^
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Open the search bar                           :guilabel:`/`
+                                              :guilabel:`Ctrl-f`
+Navigate search term history                  :guilabel:`Up`
+                                              :guilabel:`Down`
+Start the search and highlight matches        :guilabel:`Enter`
+Close the search bar without searching        :guilabel:`Ctrl-c`
+============================================  =====================
+
+Here is a view of the search bar:
+
+::
+
+  +-------------------------------------------------------------------------------+
+  |           Enter → Search  Ctrl-Alt-f → Add Filter  Ctrl-Alt-r → Clear Filters |
+  |  Search   Ctrl-t → Column:All  Ctrl-v → [ ] Invert  Ctrl-n → Matcher:REGEX    |
+  | /                                                                             |
+  +-------------------------------------------------------------------------------+
+
+Across the top are various functions with keyboard shortcuts listed. Each of
+these are clickable with the mouse. The second line shows configurable search
+parameters.
+
+**Search Parameters**
+
+- ``Column:All`` Change the part of the log message to match on. For example:
+  ``All``, ``Message`` or any extra metadata column.
+
+- ``Invert`` match. Find lines that don't match the entered text.
+
+- ``Matcher``: How the search input should be interpreted.
+
+    - ``REGEX``: Treat input text as a regex.
+
+    - ``STRING``: Treat input as a plain string. Any regex characters will be
+      escaped when search is performed.
+
+    - ``FUZZY``: input text is split on spaces using the ``.*`` regex. For
+      example if you search for ``idle run`` the resulting search regex used
+      under the hood is ``(idle)(.*?)(run)``. This would match both of these
+      lines:
+
+      .. code-block:: text
+
+         Idle task is running
+         Idle thread is running
+
+**Active Search Shortcuts**
+
+When a search is started the bar will close, log follow mode is disabled and all
+matches will be highlighted.  At this point a few extra keyboard shortcuts are
+available.
+
+============================================  =====================
+Function                                      Keys
+============================================  =====================
+Move to the next search result                :guilabel:`n`
+                                              :guilabel:`Ctrl-g`
+                                              :guilabel:`Ctrl-s`
+Move to the previous search result            :guilabel:`N`
+                                              :guilabel:`Ctrl-r`
+Removes search highlighting                   :guilabel:`Ctrl-l`
+Creates a filter using the active search      :guilabel:`Ctrl-Alt-f`
+Deletes all active filters.                   :guilabel:`Ctrl-Alt-r`
+============================================  =====================
+
+
+Log Filtering
+^^^^^^^^^^^^^
+
+Log filtering allows you to limit what log lines appear in any given log
+window. Filters can be added from the currently active search or directly in the
+search bar.
+
+- With the search bar **open**:
+
+  Type something to search for then press :guilabel:`Ctrl-Alt-f` or click on
+  :guilabel:`Add Filter`.
+
+- With the search bar **closed**:
+
+  Press :guilabel:`Ctrl-Alt-f` to use the current search term as a filter.
+
+When a filter is active the ``Filters`` toolbar will appear at the bottom of the
+log window. For example, here are some logs with one active filter for
+``lorem ipsum``.
+
+::
+
+  +------------------------------------------------------------------------------+
+  | Time               Lvl  Module  Message                                      |
+  +------------------------------------------------------------------------------+
+  | 20210722 15:38:14  INF  APP     Log message # 270 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:24  INF  APP     Log message # 280 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:34  INF  APP     Log message # 290 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:44  INF  APP     Log message # 300 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:54  INF  APP     Log message # 310 Lorem ipsum dolor sit amet |
+  | 20210722 15:39:04  INF  APP     Log message # 320 Lorem ipsum dolor sit amet |
+  +------------------------------------------------------------------------------+
+  |  Filters   <lorem ipsum (X)>  Ctrl-Alt-r → Clear Filters                     |
+  +------------------------------------------------------------------------------+
+  |   Logs   / → Search  f → [✓] Follow  t → [✓] Table  w → [ ] Wrap  C → Clear  |
+  +------------------------------------------------------------------------------+
+
+**Stacking Filters**
+
+Adding a second filter on the above logs for ``# 2`` would update the filter
+toolbar to show:
+
+::
+
+  +------------------------------------------------------------------------------+
+  | Time               Lvl  Module  Message                                      |
+  +------------------------------------------------------------------------------+
+  |                                                                              |
+  |                                                                              |
+  |                                                                              |
+  | 20210722 15:38:14  INF  APP     Log message # 270 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:24  INF  APP     Log message # 280 Lorem ipsum dolor sit amet |
+  | 20210722 15:38:34  INF  APP     Log message # 290 Lorem ipsum dolor sit amet |
+  +------------------------------------------------------------------------------+
+  |  Filters   <lorem ipsum (X)>  <# 2 (X)>  Ctrl-Alt-r → Clear Filters          |
+  +------------------------------------------------------------------------------+
+  |   Logs   / → Search  f → [✓] Follow  t → [✓] Table  w → [ ] Wrap  C → Clear  |
+  +------------------------------------------------------------------------------+
+
+Any filter listed in the Filters toolbar and can be individually removed by
+clicking on the red ``(X)`` text.
+
+
+Python Window
+~~~~~~~~~~~~~
+
+
+Running Code in the Python Repl
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+-  Type code and hit :guilabel:`Enter` to run.
+-  If multiple lines are used, move the cursor to the end and press
+   :guilabel:`Enter` twice.
+-  :guilabel:`Up` / :guilabel:`Down` Navigate command history
+-  :guilabel:`Ctrl-r` Start reverse history searching
+-  :guilabel:`Ctrl-c` Erase the input buffer
+
+   -  If the input buffer is empty:
+      :guilabel:`Ctrl-c` cancels any currently running Python commands.
+
+-  :guilabel:`F2` Open the python repl settings (from
+   `ptpython <https://github.com/prompt-toolkit/ptpython>`__). This
+   works best in vertical split mode.
+
+   -  To exit: hit :guilabel:`F2` again.
+   -  Navigate options with the arrow keys, Enter will close the menu.
+
+-  :guilabel:`F3` Open the python repl history (from
+   `ptpython <https://github.com/prompt-toolkit/ptpython>`__).
+
+   -  To exit: hit :guilabel:`F3` again.
+   -  Left side shows previously entered commands
+   -  Use arrow keys to navigate.
+   -  :guilabel:`Space` to select as many lines you want to use
+
+      -  Selected lines will be appended to the right side.
+
+   -  :guilabel:`Enter` to accept the right side text, this will be inserted
+      into the repl.
+
+
+Copy & Pasting
+~~~~~~~~~~~~~~
+
+Copying Text
+^^^^^^^^^^^^
+
+At the moment there is no built-in copy/paste function. As a workaround use your
+terminal built in selection:
+
+- **Linux**
+
+  - Holding :guilabel:`Shift` and dragging the mouse in most terminals.
+
+- **Mac**
+
+  - **Apple Terminal**:
+
+    Hold :guilabel:`Fn` and drag the mouse
+
+  - **iTerm2**:
+
+    Hold :guilabel:`Cmd+Option` and drag the mouse
+
+- **Windows**
+
+  - **Git CMD** (included in `Git for Windows <https://git-scm.com/downloads>`__)
+
+    1. Click on the Git window icon in the upper left of the title bar
+    2. Click ``Edit`` then ``Mark``
+    3. Drag the mouse to select text and press Enter to copy.
+
+  - **Windows Terminal**
+
+    1. Hold :guilabel:`Shift` and drag the mouse to select text
+    2. Press :guilabel:`Ctrl-Shift-C` to copy.
+
+Pasting Text
+^^^^^^^^^^^^
+
+Currently you must paste text using your terminal emulator's paste
+function. How to do this depends on what terminal you are using and on
+which OS. Here's how on various platforms:
+
+- **Linux**
+
+  - **XTerm**
+
+    :guilabel:`Shift-Insert` pastes text
+
+  - **Gnome Terminal**
+
+    :guilabel:`Ctrl-Shift-V` pastes text
+
+- **Windows**
+
+  - **Git CMD** (included in `Git for Windows <https://git-scm.com/downloads>`__)
+
+    1. Click on the Git icon in the upper left of the windows title bar and open
+       ``Properties``.
+    2. Checkmark the option ``Use Ctrl+Shift+C/V as Copy Paste`` and hit ok.
+    3. Then use :guilabel:`Ctrl-Shift-V` to paste.
+
+  - **Windows Terminal**
+
+   -  :guilabel:`Ctrl-Shift-V` pastes text.
+   -  :guilabel:`Shift-RightClick` also pastes text.
+
+
+Window Management
+~~~~~~~~~~~~~~~~~
+
+Any window can be hidden by clicking the checkbox in the
+:guilabel:`[x] Show Window` submenu
+
+-  To enlarge or shrink the currently focused window use :guilabel:`Ctrl-j` or
+   :guilabel:`Ctrl-k`.
+-  Reset window sizes with :guilabel:`Ctrl-u`.
+
+
+-  Use vertical window splitting with :guilabel:`F4`.
+-  Rotate window order with :guilabel:`Ctrl-Shift-Left` and
+   :guilabel:`Ctrl-Shift-Right`.
+
+
+Color Depth
+-----------
+
+Some terminals support full 24-bit color. By default pw console will try
+to use 256 colors.
+
+To force a particular color depth: set one of these environment
+variables before launching the console.
+
+::
+
+   # 1 bit | Black and white
+   export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_1_BIT
+   # 4 bit | ANSI colors
+   export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_4_BIT
+   # 8 bit | 256 colors
+   export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_8_BIT
+   # 24 bit | True colors
+   export PROMPT_TOOLKIT_COLOR_DEPTH=DEPTH_24_BIT
+
+
+Known Issues
+------------
+
+
+Python Repl Window
+~~~~~~~~~~~~~~~~~~
+
+- Any ``print()`` commands entered in the repl will not appear until the code
+  being run is completed. This is a high priority issue:
+  https://bugs.chromium.org/p/pigweed/issues/detail?id=407
+
+
+Log Window
+~~~~~~~~~~
+
+- Rendering for log lines that include ``\n`` characters is broken and hidden if
+  Table view is turned on.
+
+- Tab character rendering will not work in the log pane view. They will
+  appear as ``^I`` since prompt_toolkit can't render them. See this issue for details:
+  https://github.com/prompt-toolkit/python-prompt-toolkit/issues/556
+
+
+General
+~~~~~~~
+
+-  Mouse click and colors don't work if using Windows cmd.exe. Please
+   use the newer Windows Terminal app instead: https://github.com/microsoft/terminal
+
+
+Upcoming Features
+-----------------
+
+For upcoming features see the Pigweed Console Bug Hotlist at:
+https://bugs.chromium.org/u/542633886/hotlists/Console
+
+
+Feature Requests
+~~~~~~~~~~~~~~~~
+
+Create a feature request bugs using this template:
+https://bugs.chromium.org/p/pigweed/issues/entry?owner=tonymd@google.com&labels=Type-Enhancement,Priority-Medium&summary=pw_console
diff --git a/pw_console/py/pw_console/help_window.py b/pw_console/py/pw_console/help_window.py
index 89689ea..311ff3e 100644
--- a/pw_console/py/pw_console/help_window.py
+++ b/pw_console/py/pw_console/help_window.py
@@ -21,15 +21,18 @@
 from jinja2 import Template
 from prompt_toolkit.document import Document
 from prompt_toolkit.filters import Condition
-from prompt_toolkit.key_binding import KeyBindings
+from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
 from prompt_toolkit.layout import (
     ConditionalContainer,
     DynamicContainer,
     HSplit,
 )
 from prompt_toolkit.layout.dimension import Dimension
+from prompt_toolkit.lexers import PygmentsLexer
 from prompt_toolkit.widgets import Box, Frame, TextArea
 
+from pygments.lexers.markup import RstLexer  # type: ignore
+
 _LOG = logging.getLogger(__package__)
 
 HELP_TEMPLATE_PATH = Path(__file__).parent / "templates" / "keybind_list.jinja"
@@ -37,25 +40,55 @@
     KEYBIND_TEMPLATE = tmpl.read()
 
 
+def _longest_line_length(text):
+    """Return the longest line in the given text."""
+    max_line_length = 0
+    for line in text.splitlines():
+        if len(line) > max_line_length:
+            max_line_length = len(line)
+    return max_line_length
+
+
 class HelpWindow(ConditionalContainer):
     """Help window container for displaying keybindings."""
+    def _create_help_text_area(self, **kwargs):
+        help_text_area = TextArea(
+            focusable=True,
+            focus_on_click=True,
+            scrollbar=True,
+            style='class:help_window_content',
+            **kwargs,
+        )
+
+        # Additional keybindings for the text area.
+        key_bindings = KeyBindings()
+
+        @key_bindings.add('q')
+        def _close_window(_event: KeyPressEvent) -> None:
+            """Close the current dialog window."""
+            self.toggle_display()
+
+        help_text_area.control.key_bindings = key_bindings
+        return help_text_area
+
     def __init__(self, application, preamble='', additional_help_text=''):
         # Dict containing key = section title and value = list of key bindings.
+        self.application = application
+        self.show_window = False
         self.help_text_sections = {}
-        self.max_description_width = 0
-        self.max_key_list_width = 0
-        self.max_line_length = 0
 
         # Generated keybinding text
         self.preamble = preamble
         self.additional_help_text = additional_help_text
         self.help_text = ''
 
-        self.help_text_area = TextArea(
-            focusable=True,
-            scrollbar=True,
-            style='class:help_window_content',
-        )
+        self.max_additional_help_text_width = (_longest_line_length(
+            self.additional_help_text) if additional_help_text else 0)
+        self.max_description_width = 0
+        self.max_key_list_width = 0
+        self.max_line_length = 0
+
+        self.help_text_area = self._create_help_text_area()
 
         frame = Frame(
             body=Box(
@@ -69,9 +102,26 @@
 
         super().__init__(
             HSplit([frame]),
-            filter=Condition(lambda: application.show_help_window),
+            filter=Condition(lambda: self.show_window),
         )
 
+    def toggle_display(self):
+        """Toggle visibility of this help window."""
+        # Toggle state variable.
+        self.show_window = not self.show_window
+
+        # Set the help window in focus.
+        if self.show_window:
+            self.application.last_focused_pane = (
+                self.application.focused_window())
+            self.application.layout.focus(self.help_text_area)
+        # Restore original focus.
+        else:
+            if self.application.last_focused_pane:
+                self.application.layout.focus(
+                    self.application.last_focused_pane)
+            self.application.last_focused_pane = None
+
     def content_width(self) -> int:
         """Return total width of help window."""
         # Widths of UI elements
@@ -86,6 +136,22 @@
                                        right_side_frame_and_padding_width +
                                        scrollbar_padding + scrollbar_width)
 
+    def load_user_guide(self):
+        rstdoc = Path(__file__).parent / 'docs/user_guide.rst'
+        max_line_length = 0
+        rst_text = ''
+        with rstdoc.open() as rstfile:
+            for line in rstfile.readlines():
+                if 'https://' not in line and len(line) > max_line_length:
+                    max_line_length = len(line)
+                rst_text += line
+        self.max_line_length = max_line_length
+
+        self.help_text_area = self._create_help_text_area(
+            lexer=PygmentsLexer(RstLexer),
+            text=rst_text,
+        )
+
     def generate_help_text(self):
         """Generate help text based on added key bindings."""
 
@@ -98,6 +164,7 @@
 
         self.help_text = template.render(
             sections=self.help_text_sections,
+            max_additional_help_text_width=self.max_additional_help_text_width,
             max_description_width=self.max_description_width,
             max_key_list_width=self.max_key_list_width,
             preamble=self.preamble,
@@ -105,10 +172,7 @@
         )
 
         # Find the longest line in the rendered template.
-        self.max_line_length = 0
-        for line in self.help_text.splitlines():
-            if len(line) > self.max_line_length:
-                self.max_line_length = len(line)
+        self.max_line_length = _longest_line_length(self.help_text)
 
         # Replace the TextArea content.
         self.help_text_area.buffer.document = Document(text=self.help_text,
diff --git a/pw_console/py/pw_console/key_bindings.py b/pw_console/py/pw_console/key_bindings.py
index ec8393f..f141920 100644
--- a/pw_console/py/pw_console/key_bindings.py
+++ b/pw_console/py/pw_console/key_bindings.py
@@ -39,15 +39,11 @@
 
     bindings = KeyBindings()
 
-    @bindings.add('f1')
+    @bindings.add(
+        'f1', filter=Condition(lambda: not console_app.modal_window_is_open()))
     def show_help(event):
-        """Toggle help window."""
-        console_app.toggle_help()
-
-    @bindings.add('q', filter=Condition(lambda: console_app.show_help_window))
-    def close_help_window(event):
-        """Hide help window."""
-        console_app.toggle_help()
+        """Toggle user guide window."""
+        console_app.user_guide_window.toggle_display()
 
     # F2 is ptpython settings
     # F3 is ptpython history
diff --git a/pw_console/py/pw_console/templates/keybind_list.jinja b/pw_console/py/pw_console/templates/keybind_list.jinja
index 654b5ac..92acdb9 100644
--- a/pw_console/py/pw_console/templates/keybind_list.jinja
+++ b/pw_console/py/pw_console/templates/keybind_list.jinja
@@ -13,7 +13,7 @@
 License for the specific language governing permissions and limitations under
 the License.
 #}
-{% set total_width = max_description_width + max_key_list_width + 5 %}
+{% set total_width = [max_description_width + max_key_list_width + 5, max_additional_help_text_width]|max %}
 {% if preamble %}
 {{ preamble }}
 
diff --git a/pw_console/py/setup.py b/pw_console/py/setup.py
index 9fc2562..5166c86 100644
--- a/pw_console/py/setup.py
+++ b/pw_console/py/setup.py
@@ -24,6 +24,7 @@
     packages=setuptools.find_packages(),
     package_data={
         'pw_console': [
+            'docs/user_guide.rst',
             'py.typed',
             'templates/keybind_list.jinja',
             'templates/repl_output.jinja',
diff --git a/pw_console/testing.rst b/pw_console/testing.rst
index e2979a7..585e983 100644
--- a/pw_console/testing.rst
+++ b/pw_console/testing.rst
@@ -185,7 +185,51 @@
        | Only logs with Modules other than ``BAT`` appear.
      - |checkbox|
 
-3. Add note to the commit message
+3. Test Help Windows
+^^^^^^^^^^^^^^^^^^^^
+
+.. list-table::
+   :widths: 5 45 45 5
+   :header-rows: 1
+
+   * - #
+     - Test Action
+     - Expected Result
+     - ✅
+
+   * - 1
+     - Click the :guilabel:`Help > User Guide`
+     - | Window appears showing the user guide with
+       | RST formatting and syntax highlighting
+     - |checkbox|
+
+   * - 2
+     - Press :guilabel:`q`
+     - Window is hidden
+     - |checkbox|
+
+   * - 3
+     - Click the :guilabel:`Help > Keyboard Shortcuts`
+     - Window appears showing the keybind list
+     - |checkbox|
+
+   * - 4
+     - Press :guilabel:`q`
+     - Window is hidden
+     - |checkbox|
+
+   * - 5
+     - Click the :guilabel:`Help > Console Test Mode Help`
+     - | Window appears showing help with content
+       | ``Welcome to the Pigweed Console Test Mode!``
+     - |checkbox|
+
+   * - 6
+     - Press :guilabel:`q`
+     - Window is hidden
+     - |checkbox|
+
+4. Add note to the commit message
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 Add a ``Testing:`` line to your commit message and mention the steps