Repository: ansible/ansible-navigator (language: python)

Issue:
Bug fix for `:open` from settings menu
- Fix a bug when `:open`ing the settings menu
- The entries had not yet been converted to dictionaries
- Update fixtures and add test for opening the menu in vi


Discussion / hints:

Fix patch:
diff --git a/src/ansible_navigator/action_base.py b/src/ansible_navigator/action_base.py
index c0ed5d4f8..9661d8b3e 100644
--- a/src/ansible_navigator/action_base.py
+++ b/src/ansible_navigator/action_base.py
@@ -95,7 +95,7 @@ def no_interactive_mode(self, interaction: Interaction, app: AppPublic) -> None:
                 f"      e.g. 'ansible-navigator {self._name} --mode stdout",
             ],
         )
-        interaction.ui.show(warning)
+        interaction.ui.show_form(warning)
 
     @staticmethod
     def _copy_args(args: ApplicationConfiguration) -> ApplicationConfiguration:
diff --git a/src/ansible_navigator/actions/collections.py b/src/ansible_navigator/actions/collections.py
index 764121749..b6f3b728d 100644
--- a/src/ansible_navigator/actions/collections.py
+++ b/src/ansible_navigator/actions/collections.py
@@ -116,7 +116,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
                 "Collecting collection content, this may take a minute the first time...",
             ],
         )
-        interaction.ui.show(notification)
+        interaction.ui.show_form(notification)
 
         params = [self._name] + shlex.split(
             self._interaction.action.match.groupdict()["params"] or "",
@@ -135,7 +135,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
                     "include the contents of the log file.",
                 ],
             )
-            interaction.ui.show(notification)
+            interaction.ui.show_form(notification)
             self._prepare_to_exit(interaction)
             return None
 
diff --git a/src/ansible_navigator/actions/config.py b/src/ansible_navigator/actions/config.py
index 4d3651974..9121744ed 100644
--- a/src/ansible_navigator/actions/config.py
+++ b/src/ansible_navigator/actions/config.py
@@ -112,7 +112,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
                 "Collecting the ansible configuration, this may take a minute...",
             ],
         )
-        interaction.ui.show(notification)
+        interaction.ui.show_form(notification)
 
         self._update_args(
             [self._name] + shlex.split(self._interaction.action.match.groupdict()["params"] or ""),
@@ -269,7 +269,7 @@ def _run_runner(self) -> Optional[Tuple]:
                 if not list_output or not dump_output:
                     warn_msg.append("The configuration could not be gathered.")
                 warning = warning_notification(warn_msg)
-                self._interaction.ui.show(warning)
+                self._interaction.ui.show_form(warning)
             else:
                 self._parse_and_merge(list_output, dump_output)
         else:
diff --git a/src/ansible_navigator/actions/doc.py b/src/ansible_navigator/actions/doc.py
index 475d8e26a..d6a889ddd 100644
--- a/src/ansible_navigator/actions/doc.py
+++ b/src/ansible_navigator/actions/doc.py
@@ -109,7 +109,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
 
         plugin_doc = self._run_runner()
 
-        if not plugin_doc:
+        if not isinstance(plugin_doc, dict):
             self._prepare_to_exit(interaction)
             return None
 
diff --git a/src/ansible_navigator/actions/help_doc.py b/src/ansible_navigator/actions/help_doc.py
index e3f40a1c7..eaa2034f7 100644
--- a/src/ansible_navigator/actions/help_doc.py
+++ b/src/ansible_navigator/actions/help_doc.py
@@ -1,6 +1,7 @@
 """``:help`` command implementation."""
 import os
 
+from ansible_navigator.content_defs import ContentFormat
 from ..action_base import ActionBase
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
@@ -40,7 +41,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
         while True:
             interaction = interaction.ui.show(
                 obj=help_md,
-                serialization_format="text.html.markdown",
+                content_format=ContentFormat.MARKDOWN,
             )
             app.update()
             if interaction.name != "refresh":
diff --git a/src/ansible_navigator/actions/images.py b/src/ansible_navigator/actions/images.py
index 4286ea846..0573dd03f 100644
--- a/src/ansible_navigator/actions/images.py
+++ b/src/ansible_navigator/actions/images.py
@@ -139,7 +139,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
         notification = nonblocking_notification(
             messages=["Collecting available images, this may take a minute..."],
         )
-        interaction.ui.show(notification)
+        interaction.ui.show_form(notification)
 
         self._collect_image_list()
         if not self._images.value:
@@ -148,7 +148,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
             ]
             messages.append("Please check the log (:log) for errors.")
             warning = warning_notification(messages=messages)
-            interaction.ui.show(warning)
+            interaction.ui.show_form(warning)
             self._logger.error(messages[0])
             return None
 
@@ -221,7 +221,7 @@ def _build_image_content(self) -> Step:
         if not self._images.selected["__introspected"]:
             message = "Collecting image details, this may take a minute..."
             notification = nonblocking_notification(messages=[message])
-            self._interaction.ui.show(notification)
+            self._interaction.ui.show_form(notification)
             introspection_success = self._introspect_image()
             if introspection_success is False:
                 return self.steps.previous
diff --git a/src/ansible_navigator/actions/inventory.py b/src/ansible_navigator/actions/inventory.py
index b948c6930..b9aa8dcfd 100644
--- a/src/ansible_navigator/actions/inventory.py
+++ b/src/ansible_navigator/actions/inventory.py
@@ -12,6 +12,7 @@
 from typing import Tuple
 from typing import Union
 
+from ansible_navigator.content_defs import ContentFormat
 from ..action_base import ActionBase
 from ..action_defs import RunStdoutReturn
 from ..app_public import AppPublic
@@ -203,7 +204,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
             while True:
                 interaction = self._interaction.ui.show(
                     self._inventory_error,
-                    serialization_format="source.ansi",
+                    content_format=ContentFormat.ANSI,
                 )
                 if interaction.name != "refresh":
                     break
@@ -472,7 +473,7 @@ def _build_inventory_list(self) -> None:
                 form_dict["fields"].append(form_field)
 
             form = dict_to_form(form_dict)
-            self._interaction.ui.show(form)
+            self._interaction.ui.show_form(form)
 
             if form.cancelled:
                 return
@@ -516,7 +517,7 @@ def _collect_inventory_details_interactive(
         self._logger.error(" ".join(warn_msg))
         if "ERROR!" in inventory_err or "Error" in inventory_err:
             warning = warning_notification(warn_msg)
-            self._interaction.ui.show(warning)
+            self._interaction.ui.show_form(warning)
         else:
             self._extract_inventory(inventory_output)
 
diff --git a/src/ansible_navigator/actions/log.py b/src/ansible_navigator/actions/log.py
index 9d8129d7d..e8c299dac 100644
--- a/src/ansible_navigator/actions/log.py
+++ b/src/ansible_navigator/actions/log.py
@@ -1,4 +1,5 @@
 """``:log`` command implementation."""
+from ansible_navigator.content_defs import ContentFormat
 from ..action_base import ActionBase
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
@@ -39,7 +40,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
             if auto_scroll:
                 interaction.ui.scroll(new_scroll)
 
-            interaction = interaction.ui.show(obj=log_contents, serialization_format="text.log")
+            interaction = interaction.ui.show(obj=log_contents, content_format=ContentFormat.LOG)
             if interaction.name != "refresh":
                 break
 
diff --git a/src/ansible_navigator/actions/open_file.py b/src/ansible_navigator/actions/open_file.py
index d6d84bcb9..58a52f68a 100644
--- a/src/ansible_navigator/actions/open_file.py
+++ b/src/ansible_navigator/actions/open_file.py
@@ -11,13 +11,13 @@
 from typing import List
 from typing import Optional
 
+from ansible_navigator.content_defs import ContentFormat
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
-from ..ui_framework import ContentView
+from ..content_defs import ContentView
 from ..ui_framework import Interaction
 from ..ui_framework import Menu
 from ..utils.functions import remove_dbl_un
-from ..utils.serialize import SerializationFormat
 from ..utils.serialize import serialize_write_temp_file
 from . import _actions as actions
 
@@ -102,7 +102,8 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
                 return None
 
         if not filename:
-            if interaction.ui.serialization_format() == "text.html.markdown":
+            content_format = interaction.ui.content_format()
+            if content_format is ContentFormat.MARKDOWN:
                 with tempfile.NamedTemporaryFile(
                     suffix=".md",
                     delete=False,
@@ -110,17 +111,11 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
                 ) as file_like:
                     filename = Path(file_like.name)
                     file_like.write(obj)
-            elif interaction.ui.serialization_format() == "source.yaml":
+            elif content_format.value.serialization:
                 filename = serialize_write_temp_file(
                     content=obj,
                     content_view=ContentView.NORMAL,
-                    serialization_format=SerializationFormat.YAML,
-                )
-            elif interaction.ui.serialization_format() == "source.json":
-                filename = serialize_write_temp_file(
-                    content=obj,
-                    content_view=ContentView.NORMAL,
-                    serialization_format=SerializationFormat.JSON,
+                    serialization_format=content_format.value.serialization,
                 )
             else:
                 with tempfile.NamedTemporaryFile(
diff --git a/src/ansible_navigator/actions/run.py b/src/ansible_navigator/actions/run.py
index 27621e5ee..a83dab56a 100644
--- a/src/ansible_navigator/actions/run.py
+++ b/src/ansible_navigator/actions/run.py
@@ -20,11 +20,12 @@
 from typing import Optional
 from typing import Tuple
 
-from ansible_navigator.ui_framework.content_defs import ContentView
 from ..action_base import ActionBase
 from ..action_defs import RunStdoutReturn
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import ContentView
+from ..content_defs import SerializationFormat
 from ..runner import CommandAsync
 from ..steps import Step
 from ..ui_framework import CursesLine
@@ -40,7 +41,6 @@
 from ..utils.functions import now_iso
 from ..utils.functions import remove_ansi
 from ..utils.functions import round_half_up
-from ..utils.serialize import SerializationFormat
 from ..utils.serialize import serialize_write_file
 from . import _actions as actions
 from . import run_action
@@ -299,7 +299,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
         if self._subaction_type == "run":
             messages = ["Preparing for automation, please wait..."]
             notification = nonblocking_notification(messages=messages)
-            interaction.ui.show(notification)
+            interaction.ui.show_form(notification)
             while not self._first_message_received:
                 self.update()
 
@@ -458,7 +458,7 @@ def _prompt_for_artifact(self, artifact_file: str) -> Dict[Any, Any]:
         }
         form_dict["fields"].append(form_field)
         form = dict_to_form(form_dict)
-        self._interaction.ui.show(form)
+        self._interaction.ui.show_form(form)
         populated_form = form_to_dict(form, key_on_name=True)
         return populated_form
 
@@ -515,7 +515,7 @@ def _prompt_for_playbook(self) -> Dict[Any, Any]:
         }
         form_dict["fields"].append(form_field)
         form = dict_to_form(form_dict)
-        self._interaction.ui.show(form)
+        self._interaction.ui.show_form(form)
         populated_form = form_to_dict(form, key_on_name=True)
         return populated_form
 
@@ -906,4 +906,4 @@ def _notify_error(self, message: str):
         warn_msg += ["[HINT] To see the full error message try ':stdout'"]
         warn_msg += ["[HINT] After it's fixed, try to ':rerun' the playbook"]
         warning = warning_notification(warn_msg)
-        self._interaction.ui.show(warning)
+        self._interaction.ui.show_form(warning)
diff --git a/src/ansible_navigator/actions/sample_form.py b/src/ansible_navigator/actions/sample_form.py
index 4466fee5b..ab67f5fe2 100644
--- a/src/ansible_navigator/actions/sample_form.py
+++ b/src/ansible_navigator/actions/sample_form.py
@@ -108,7 +108,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
 
         form_data = yaml.safe_load(FORM)
         form = dict_to_form(form_data["form"])
-        interaction.ui.show(form)
+        interaction.ui.show_form(form)
         as_dict = form_to_dict(form)
         self._logger.debug("form response: %s", as_dict)
 
diff --git a/src/ansible_navigator/actions/sample_notification.py b/src/ansible_navigator/actions/sample_notification.py
index 532085821..d46f1ba4a 100644
--- a/src/ansible_navigator/actions/sample_notification.py
+++ b/src/ansible_navigator/actions/sample_notification.py
@@ -54,7 +54,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
 
         form_data = yaml.safe_load(FORM)
         form = dict_to_form(form_data["form"])
-        interaction.ui.show(form)
+        interaction.ui.show_form(form)
         as_dict = form_to_dict(form)
         self._logger.debug("form response: %s", as_dict)
 
diff --git a/src/ansible_navigator/actions/sample_working.py b/src/ansible_navigator/actions/sample_working.py
index 1bb45db70..fad32894d 100644
--- a/src/ansible_navigator/actions/sample_working.py
+++ b/src/ansible_navigator/actions/sample_working.py
@@ -34,7 +34,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
         messages = ["Please wait, this won't take long, about 3 seconds, really."]
 
         form = nonblocking_notification(messages)
-        interaction.ui.show(form)
+        interaction.ui.show_form(form)
         time.sleep(3)
 
         self._prepare_to_exit(interaction)
diff --git a/src/ansible_navigator/actions/serialize_json.py b/src/ansible_navigator/actions/serialize_json.py
index b4eb6c79d..a68140e62 100644
--- a/src/ansible_navigator/actions/serialize_json.py
+++ b/src/ansible_navigator/actions/serialize_json.py
@@ -3,6 +3,7 @@
 
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import ContentFormat
 from ..ui_framework import Interaction
 from . import _actions as actions
 
@@ -31,5 +32,5 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
         self._logger.debug("json requested")
         if interaction.ui is not None:
             interaction.ui.scroll(0)
-            serialization_format = interaction.ui.serialization_format("source.json", default=True)
-            self._logger.debug("Serialization set to %s", serialization_format)
+            content_format = interaction.ui.content_format(ContentFormat.JSON, default=True)
+            self._logger.debug("Content format set to %s", content_format)
diff --git a/src/ansible_navigator/actions/serialize_yaml.py b/src/ansible_navigator/actions/serialize_yaml.py
index df4389df1..08507b6a3 100644
--- a/src/ansible_navigator/actions/serialize_yaml.py
+++ b/src/ansible_navigator/actions/serialize_yaml.py
@@ -1,6 +1,7 @@
 """``:yaml`` command implementation."""
 import logging
 
+from ansible_navigator.content_defs import ContentFormat
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
 from ..ui_framework import Interaction
@@ -31,5 +32,5 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
         self._logger.debug("yaml requested")
         if interaction.ui is not None:
             interaction.ui.scroll(0)
-            serialization_format = interaction.ui.serialization_format("source.yaml", default=True)
-            self._logger.debug("Serialization set to %s", serialization_format)
+            content_format = interaction.ui.content_format(ContentFormat.YAML, default=True)
+            self._logger.debug("Serialization set to %s", content_format)
diff --git a/src/ansible_navigator/actions/settings.py b/src/ansible_navigator/actions/settings.py
index dd6532bf8..21658948e 100644
--- a/src/ansible_navigator/actions/settings.py
+++ b/src/ansible_navigator/actions/settings.py
@@ -9,16 +9,16 @@
 from ..configuration_subsystem import PresentableSettingsEntries
 from ..configuration_subsystem import PresentableSettingsEntry
 from ..configuration_subsystem import to_presentable
+from ..content_defs import ContentView
+from ..content_defs import SerializationFormat
 from ..steps import StepType
 from ..steps import TypedStep
 from ..ui_framework import Color
-from ..ui_framework import ContentView
 from ..ui_framework import CursesLine
 from ..ui_framework import CursesLinePart
 from ..ui_framework import CursesLines
 from ..ui_framework import Decoration
 from ..ui_framework import Interaction
-from ..utils.serialize import SerializationFormat
 from ..utils.serialize import serialize
 from . import _actions as actions
 from . import run_action
diff --git a/src/ansible_navigator/actions/stdout.py b/src/ansible_navigator/actions/stdout.py
index ff9e34c12..1b5c8459f 100644
--- a/src/ansible_navigator/actions/stdout.py
+++ b/src/ansible_navigator/actions/stdout.py
@@ -3,6 +3,7 @@
 from ..action_base import ActionBase
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import ContentFormat
 from ..ui_framework import Interaction
 from . import _actions as actions
 
@@ -40,7 +41,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
             obj = "\n".join(app.stdout)
             next_interaction: Interaction = interaction.ui.show(
                 obj=obj,
-                serialization_format="source.ansi",
+                content_format=ContentFormat.ANSI,
             )
             if next_interaction.name != "refresh":
                 break
diff --git a/src/ansible_navigator/actions/template.py b/src/ansible_navigator/actions/template.py
index 001d6e6d5..ae3560e25 100644
--- a/src/ansible_navigator/actions/template.py
+++ b/src/ansible_navigator/actions/template.py
@@ -10,6 +10,7 @@
 from ..action_base import ActionBase
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import ContentFormat
 from ..ui_framework import Interaction
 from ..ui_framework import warning_notification
 from ..utils.functions import remove_dbl_un
@@ -78,7 +79,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
         if errors:
             msgs = ["Errors encountered while templating input"] + errors
             msgs.extend(type_msgs)
-            interaction.ui.show(warning_notification(msgs))
+            interaction.ui.show_form(warning_notification(msgs))
             return None
 
         if isinstance(templated, str):
@@ -86,10 +87,10 @@ def run(self, interaction: Interaction, app: AppPublic) -> Optional[Interaction]
 
         while True:
             app.update()
-            serialization_format = "source.txt" if isinstance(templated, str) else ""
+            serialization_format = ContentFormat.TXT if isinstance(templated, str) else None
             next_interaction: Interaction = interaction.ui.show(
                 obj=templated,
-                serialization_format=serialization_format,
+                content_format=serialization_format,
             )
             if next_interaction.name != "refresh":
                 break
diff --git a/src/ansible_navigator/actions/welcome.py b/src/ansible_navigator/actions/welcome.py
index cc511bd9c..e8c5455ce 100644
--- a/src/ansible_navigator/actions/welcome.py
+++ b/src/ansible_navigator/actions/welcome.py
@@ -4,6 +4,7 @@
 from ..action_base import ActionBase
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import ContentFormat
 from ..ui_framework import Interaction
 from . import _actions as actions
 
@@ -46,7 +47,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> Interaction:
             self._calling_app.update()
             interaction = interaction.ui.show(
                 obj=welcome_md,
-                serialization_format="text.html.markdown",
+                content_format=ContentFormat.MARKDOWN,
             )
             app.update()
             if interaction.name != "refresh":
diff --git a/src/ansible_navigator/actions/write_file.py b/src/ansible_navigator/actions/write_file.py
index 3569c7fad..0cd6918ec 100644
--- a/src/ansible_navigator/actions/write_file.py
+++ b/src/ansible_navigator/actions/write_file.py
@@ -5,12 +5,12 @@
 
 from pathlib import Path
 
-from ansible_navigator.ui_framework.content_defs import ContentView
+from ansible_navigator.content_defs import ContentView
 from ..app_public import AppPublic
 from ..configuration_subsystem import ApplicationConfiguration
+from ..content_defs import SerializationFormat
 from ..ui_framework import Interaction
 from ..utils.functions import remove_dbl_un
-from ..utils.serialize import SerializationFormat
 from ..utils.serialize import serialize_write_file
 from . import _actions as actions
 
@@ -73,19 +73,19 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
                 ]
 
         if isinstance(obj, str):
-            write_as = "text"
+            write_as = ".txt"
         else:
             if re.match(r"^.*\.y(?:a)?ml$", filename):
-                write_as = "yaml"
+                write_as = ".yaml"
             elif filename.endswith(".json"):
-                write_as = "json"
+                write_as = ".json"
             else:
-                write_as = interaction.ui.serialization_format()
+                write_as = interaction.ui.content_format().value.file_extention
 
-        if write_as == "text":
+        if write_as == ".txt":
             with open(os.path.abspath(filename), file_mode, encoding="utf-8") as fh:
                 fh.write(obj)
-        elif write_as == "yaml":
+        elif write_as == ".yaml":
             serialize_write_file(
                 content=obj,
                 content_view=ContentView.NORMAL,
@@ -93,7 +93,7 @@ def run(self, interaction: Interaction, app: AppPublic) -> None:
                 file_mode=file_mode,
                 serialization_format=SerializationFormat.YAML,
             )
-        elif write_as == "json":
+        elif write_as == ".json":
             serialize_write_file(
                 content=obj,
                 content_view=ContentView.NORMAL,
diff --git a/src/ansible_navigator/configuration_subsystem/defs_presentable.py b/src/ansible_navigator/configuration_subsystem/defs_presentable.py
index e372c1013..5f65a0eda 100644
--- a/src/ansible_navigator/configuration_subsystem/defs_presentable.py
+++ b/src/ansible_navigator/configuration_subsystem/defs_presentable.py
@@ -11,7 +11,7 @@
 from typing import TypeVar
 from typing import Union
 
-from ..ui_framework import ContentBase
+from ..content_defs import ContentBase
 from .definitions import CliParameters
 from .definitions import Constants as C
 from .definitions import SettingsEntry
diff --git a/src/ansible_navigator/ui_framework/content_defs.py b/src/ansible_navigator/content_defs.py
similarity index 66%
rename from src/ansible_navigator/ui_framework/content_defs.py
rename to src/ansible_navigator/content_defs.py
index 27e60716c..9b8524a55 100644
--- a/src/ansible_navigator/ui_framework/content_defs.py
+++ b/src/ansible_navigator/content_defs.py
@@ -4,15 +4,18 @@
 from dataclasses import dataclass
 from enum import Enum
 from typing import TYPE_CHECKING
+from typing import Any
 from typing import Dict
 from typing import Generic
+from typing import List
+from typing import Optional
+from typing import Sequence
 from typing import TypeVar
-
-from ..utils.serialize import SerializationFormat
+from typing import Union
 
 
 if TYPE_CHECKING:
-    from ..utils.compatibility import TypeAlias
+    from .utils.compatibility import TypeAlias
 
 
 class ContentView(Enum):
@@ -26,6 +29,13 @@ class ContentView(Enum):
 DictType: "TypeAlias" = Dict[str, T]
 
 
+class SerializationFormat(Enum):
+    """The serialization format."""
+
+    YAML = "YAML"
+    JSON = "JSON"
+
+
 @dataclass
 class ContentBase(Generic[T]):
     r"""The base class for all content dataclasses presented in the UI.
@@ -112,3 +122,50 @@ def get(self, attribute: str):
         :returns: The gotten attribute
         """
         return getattr(self, attribute)
+
+    def items(self):
+        """Allow this dataclass to be treated like a dictionary.
+
+        This is a work around until the UI fully supports dataclasses
+        at which time this can be removed.
+
+        :returns: Tuples of attribute value pairs
+        """
+        return asdict(self).items()
+
+
+ContentTypeSingle = Union[bool, float, int, str, Dict[str, Any], ContentBase]
+ContentTypeSequence = Union[List[Any], Sequence[ContentBase]]
+ContentType = Union[ContentTypeSingle, ContentTypeSequence]
+
+
+@dataclass(frozen=True)
+class CFormat:
+    """A single instance for a content format."""
+
+    scope: str
+    """The scope, used for tokenization"""
+    file_extention: str
+    """The file extension, with a ."""
+    serialization: Optional[SerializationFormat]
+    """If needed the serialization format"""
+
+
+class ContentFormat(Enum):
+    """All content formats."""
+
+    value: CFormat
+    ANSI = CFormat(scope="source.ansi", file_extention=".ansi", serialization=None)
+    JSON = CFormat(
+        scope="source.json",
+        file_extention=".json",
+        serialization=SerializationFormat.JSON,
+    )
+    LOG = CFormat(scope="text.log", file_extention=".log", serialization=None)
+    MARKDOWN = CFormat(scope="text.html.markdown", file_extention=".md", serialization=None)
+    TXT = CFormat(scope="source.txt", file_extention=".txt", serialization=None)
+    YAML = CFormat(
+        scope="source.yaml",
+        file_extention=".yml",
+        serialization=SerializationFormat.YAML,
+    )
diff --git a/src/ansible_navigator/ui_framework/__init__.py b/src/ansible_navigator/ui_framework/__init__.py
index 49c1e1e38..3182a6010 100644
--- a/src/ansible_navigator/ui_framework/__init__.py
+++ b/src/ansible_navigator/ui_framework/__init__.py
@@ -1,8 +1,6 @@
 """ui_framework
 """
 
-from .content_defs import ContentBase
-from .content_defs import ContentView
 from .curses_defs import CursesLine
 from .curses_defs import CursesLinePart
 from .curses_defs import CursesLines
@@ -26,8 +24,6 @@
     "Action",
     "Color",
     "Content",
-    "ContentBase",
-    "ContentView",
     "CursesLine",
     "CursesLinePart",
     "CursesLines",
diff --git a/src/ansible_navigator/ui_framework/menu_builder.py b/src/ansible_navigator/ui_framework/menu_builder.py
index 555585740..78be95399 100644
--- a/src/ansible_navigator/ui_framework/menu_builder.py
+++ b/src/ansible_navigator/ui_framework/menu_builder.py
@@ -9,7 +9,10 @@
 from typing import Dict
 from typing import List
 from typing import Tuple
+from typing import Union
 
+from ..content_defs import ContentBase
+from ..content_defs import ContentTypeSequence
 from .curses_defs import CursesLine
 from .curses_defs import CursesLinePart
 from .curses_defs import CursesLines
@@ -44,11 +47,21 @@ def __init__(
         self._color_menu_item = color_menu_item
         self._ui_config = ui_config
 
-    def build(self, dicts: List, cols: List, indices) -> Tuple[CursesLines, CursesLines]:
+    def build(
+        self,
+        dicts: ContentTypeSequence,
+        cols: List[str],
+        indices,
+    ) -> Tuple[CursesLines, CursesLines]:
         """main entry point for menu builder"""
         return self._menu(dicts, cols, indices)
 
-    def _menu(self, dicts: List, cols: List[str], indices) -> Tuple[CursesLines, CursesLines]:
+    def _menu(
+        self,
+        dicts: ContentTypeSequence,
+        cols: List[str],
+        indices,
+    ) -> Tuple[CursesLines, CursesLines]:
         """Build a text menu from a list of dicts given columns(root keys)
 
         :param dicts: A list of dicts
@@ -130,7 +143,12 @@ def _menu_header_line_part(colno: int, menu_layout: Tuple[List, ...]) -> CursesL
             decoration=curses.A_UNDERLINE,
         )
 
-    def _menu_lines(self, dicts: List[Dict], menu_layout: Tuple[List, ...], indices) -> CursesLines:
+    def _menu_lines(
+        self,
+        dicts: ContentTypeSequence,
+        menu_layout: Tuple[List, ...],
+        indices,
+    ) -> CursesLines:
         """Generate all the menu lines
 
         :params dicts: A list of dicts from which the menu will be generated
@@ -144,7 +162,11 @@ def _menu_lines(self, dicts: List[Dict], menu_layout: Tuple[List, ...], indices)
         """
         return CursesLines(tuple(self._menu_line(dicts[idx], menu_layout) for idx in indices))
 
-    def _menu_line(self, menu_entry: dict, menu_layout: Tuple[List, ...]) -> CursesLine:
+    def _menu_line(
+        self,
+        menu_entry: Union[Dict[str, Any], ContentBase],
+        menu_layout: Tuple[List, ...],
+    ) -> CursesLine:
         """Generate one the menu line
 
         :param menu_entry: One dict from which the menu line will be generated
@@ -168,7 +190,7 @@ def _menu_line_part(
         self,
         colno: int,
         coltext: Any,
-        menu_entry: dict,
+        menu_entry: Union[Dict[str, Any], ContentBase],
         menu_layout: Tuple[List, ...],
     ) -> CursesLinePart:
         """Generate one menu line part
diff --git a/src/ansible_navigator/ui_framework/ui.py b/src/ansible_navigator/ui_framework/ui.py
index 2326b9413..c8151194e 100644
--- a/src/ansible_navigator/ui_framework/ui.py
+++ b/src/ansible_navigator/ui_framework/ui.py
@@ -18,15 +18,19 @@
 from typing import NamedTuple
 from typing import Optional
 from typing import Pattern
+from typing import Sequence
 from typing import Tuple
 from typing import Union
 
+from ..content_defs import ContentFormat
+from ..content_defs import ContentType
+from ..content_defs import ContentTypeSequence
+from ..content_defs import ContentView
+from ..utils.compatibility import Protocol
 from ..utils.functions import templar
-from ..utils.serialize import SerializationFormat
 from ..utils.serialize import serialize
 from .colorize import Colorize
 from .colorize import rgb_to_ansi
-from .content_defs import ContentView
 from .curses_defs import CursesLine
 from .curses_defs import CursesLinePart
 from .curses_defs import CursesLines
@@ -67,8 +71,37 @@ class Content(NamedTuple):
 class Menu(NamedTuple):
     """details about the currently showing menu"""
 
-    current: List
-    columns: List
+    current: ContentTypeSequence
+    columns: List[str]
+
+
+class ContentFormatCallable(Protocol):
+    """Protocol definition for the Ui.content_format callable."""
+
+    def __call__(
+        self,
+        value: Optional[ContentFormat] = None,
+        default: bool = False,
+    ) -> ContentFormat:
+        """Refer to and keep in sync with UserInterface.content_format"""
+
+
+class ShowCallable(Protocol):
+    """Protocol definition for the Ui.show callable."""
+
+    # pylint: disable=too-many-arguments
+    def __call__(
+        self,
+        obj: ContentType,
+        content_format: Optional[ContentFormat] = None,
+        index: Optional[int] = None,
+        columns: Optional[List] = None,
+        await_input: bool = True,
+        filter_content_keys: Callable = lambda x: x,
+        color_menu_item: Callable = lambda *args, **kwargs: (0, 0),
+        content_heading: Callable = lambda *args, **kwargs: None,
+    ) -> "Interaction":
+        """Refer to and keep in sync with UserInterface.show"""
 
 
 class Ui(NamedTuple):
@@ -77,9 +110,10 @@ class Ui(NamedTuple):
     clear: Callable
     menu_filter: Callable
     scroll: Callable
-    show: Callable
+    show: ShowCallable
+    show_form: Callable[[Form], Form]
     update_status: Callable
-    serialization_format: Callable
+    content_format: ContentFormatCallable
 
 
 class Interaction(NamedTuple):
@@ -125,7 +159,7 @@ def __init__(
         self._content_heading: Callable[[Any, int], Optional[CursesLines]]
         self._default_colors = None
         self._default_pairs = None
-        self._default_obj_serialization = "source.yaml"
+        self._default_content_format = ContentFormat.YAML
         self._filter_content_keys: Callable[[Any], Dict[Any, Any]]
         self._hide_keys = True
         self._kegexes = kegexes
@@ -140,7 +174,7 @@ def __init__(
         self._rgb_to_curses_color_idx: Dict[RgbTuple, int] = {}
         self._screen_min_height = screen_min_height
         self._scroll = 0
-        self._serialization_format = self._default_obj_serialization
+        self._content_format = self._default_content_format
         self._status = ""
         self._status_color = 0
         self._screen: Window = curses.initscr()
@@ -201,18 +235,21 @@ def scroll(self, value: Optional[int] = None) -> int:
             self._scroll = value
         return self._scroll
 
-    def serialization_format(self, value: Optional[str] = None, default: bool = False) -> str:
-        """Set or return the current serialization format
+    def content_format(
+        self,
+        value: Optional[ContentFormat] = None,
+        default: bool = False,
+    ) -> ContentFormat:
+        """Set or return the current content format
 
-        :param value: the value to set the serialization format to
-        :type value: str or None
-        :returns: the current serialization format
+        :param value: The value to set the content format to
+        :returns: The current content format
         """
         if value is not None:
-            self._serialization_format = value
+            self._content_format = value
             if default:
-                self._default_obj_serialization = value
-        return self._serialization_format
+                self._default_content_format = value
+        return self._content_format
 
     @property
     def _ui(self) -> Ui:
@@ -225,8 +262,9 @@ def _ui(self) -> Ui:
             menu_filter=self.menu_filter,
             scroll=self.scroll,
             show=self.show,
+            show_form=self.show_form,
             update_status=self.update_status,
-            serialization_format=self.serialization_format,
+            content_format=self.content_format,
         )
         return res
 
@@ -509,28 +547,23 @@ def _serialize_color(self, obj: Any) -> CursesLines:
         :returns: The generated lines
         """
 
-        if self.serialization_format() == "source.ansi":
+        if self.content_format() is ContentFormat.ANSI:
             return self._colorizer.render_ansi(doc=obj)
 
         content_view = ContentView.NORMAL if self._hide_keys else ContentView.FULL
-        if self.serialization_format() == "source.yaml":
-            string = serialize(
-                content_view=content_view,
-                content=obj,
-                serialization_format=SerializationFormat.YAML,
-            )
-        elif self.serialization_format() == "source.json":
+        current_format = self.content_format()
+        if current_format.value.serialization:
             string = serialize(
                 content_view=content_view,
                 content=obj,
-                serialization_format=SerializationFormat.JSON,
+                serialization_format=current_format.value.serialization,
             )
         else:
             string = obj
 
         scope = "no_color"
         if self._ui_config.color:
-            scope = self.serialization_format()
+            scope = self.content_format().value.scope
 
         rendered = self._colorizer.render(doc=string, scope=scope)
         return self._color_lines_for_term(rendered)
@@ -630,7 +663,12 @@ def _show_form(self, obj: Form) -> Form:
         res = obj.present(screen=self._screen, ui_config=self._ui_config)
         return res
 
-    def _show_obj_from_list(self, objs: List[Any], index: int, await_input: bool) -> Interaction:
+    def _show_obj_from_list(
+        self,
+        objs: ContentTypeSequence,
+        index: int,
+        await_input: bool,
+    ) -> Interaction:
         # pylint: disable=too-many-branches
         # pylint: disable=too-many-locals
         # pylint: disable=too-many-statements
@@ -758,7 +796,7 @@ def _search_value(regex: Pattern, value: str) -> Optional[Match]:
 
     def _get_heading_menu_items(
         self,
-        current: List,
+        current: Sequence[Any],
         columns: List,
         indices,
     ) -> Tuple[CursesLines, CursesLines]:
@@ -778,7 +816,7 @@ def _get_heading_menu_items(
         menu_heading, menu_items = menu_builder.build(current, columns, indices)
         return menu_heading, menu_items
 
-    def _show_menu(self, current: List, columns: List, await_input: bool) -> Interaction:
+    def _show_menu(self, current: Sequence[Any], columns: List, await_input: bool) -> Interaction:
         """Show a menu on the screen
 
         :param current: A dict
@@ -839,19 +877,19 @@ def _show_menu(self, current: List, columns: List, await_input: bool) -> Interac
 
     def show(
         self,
-        obj: Union[List, Dict, str, bool, int, float],
-        serialization_format: str = "",
+        obj: ContentType,
+        content_format: Optional[ContentFormat] = None,
         index: Optional[int] = None,
         columns: Optional[List] = None,
         await_input: bool = True,
         filter_content_keys: Callable = lambda x: x,
         color_menu_item: Callable = lambda *args, **kwargs: (0, 0),
         content_heading: Callable = lambda *args, **kwargs: None,
-    ) -> Union[Interaction, Form]:
+    ) -> Interaction:
         """Show something on the screen
 
         :param obj: The inbound object
-        :param serialization_format: Set the serialization format
+        :param content_format: Set the content format
         :param index: When obj is a list, show this entry
         :param columns: When obj is a list of dicts, use these keys for menu columns
         :param await_input: Should we wait for user input?
@@ -861,11 +899,7 @@ def show(
         self._content_heading = content_heading
         self._filter_content_keys = filter_content_keys
         columns = columns or []
-        self.serialization_format(serialization_format or self._default_obj_serialization)
-
-        if isinstance(obj, Form):
-            form_result = self._show_form(obj)
-            return form_result
+        self.content_format(content_format or self._default_content_format)
 
         if index is not None and isinstance(obj, (list, tuple)):
             result = self._show_obj_from_list(obj, index, await_input)
@@ -874,3 +908,12 @@ def show(
         else:
             result = self._show_obj_from_list([obj], 0, await_input)
         return result
+
+    def show_form(self, form: Form) -> Form:
+        """Show a form on using the user interface.
+
+        :param form: The form to show
+        :returns: The form populated with the response
+        """
+        form_result = self._show_form(form)
+        return form_result
diff --git a/src/ansible_navigator/ui_framework/utils.py b/src/ansible_navigator/ui_framework/utils.py
index d38b9d26b..39cb6706b 100644
--- a/src/ansible_navigator/ui_framework/utils.py
+++ b/src/ansible_navigator/ui_framework/utils.py
@@ -10,7 +10,7 @@
 from typing import List
 from typing import Union
 
-from .content_defs import ContentBase
+from ..content_defs import ContentBase
 
 
 def convert_percentage(
diff --git a/src/ansible_navigator/utils/compatibility.py b/src/ansible_navigator/utils/compatibility.py
index c3b419413..7a8bbc231 100644
--- a/src/ansible_navigator/utils/compatibility.py
+++ b/src/ansible_navigator/utils/compatibility.py
@@ -14,6 +14,15 @@
     else:
         from typing_extensions import TypeAlias
 
+if sys.version_info < (3, 8):
+    if TYPE_CHECKING:
+        from typing_extensions import Protocol
+    else:
+        Protocol = object
+
+else:
+    from typing import Protocol
+
 
 if sys.version_info < (3, 9):
     from backports import zoneinfo
diff --git a/src/ansible_navigator/utils/serialize.py b/src/ansible_navigator/utils/serialize.py
index 0e4e68fcc..c16b8ceaa 100644
--- a/src/ansible_navigator/utils/serialize.py
+++ b/src/ansible_navigator/utils/serialize.py
@@ -6,34 +6,21 @@
 import tempfile
 
 from dataclasses import is_dataclass
-from enum import Enum
 from pathlib import Path
 from typing import IO
-from typing import TYPE_CHECKING
 from typing import Any
-from typing import Dict
-from typing import List
 from typing import NamedTuple
 from typing import Optional
-from typing import Union
 
 import yaml
 
+from ..content_defs import ContentBase
+from ..content_defs import ContentType
+from ..content_defs import ContentView
+from ..content_defs import SerializationFormat
 
-logger = logging.getLogger(__name__)
-
-
-class SerializationFormat(Enum):
-    """The serialization format."""
-
-    YAML = "YAML"
-    JSON = "JSON"
-
-
-if TYPE_CHECKING:
-    from ..ui_framework import ContentBase
-    from ..ui_framework import ContentView
 
+logger = logging.getLogger(__name__)
 
 # pylint: disable=unused-import
 try:
@@ -49,21 +36,10 @@ class SerializationFormat(Enum):
     from yaml import SafeLoader  # type: ignore[misc] # noqa: F401
 # pylint: enable=unused-import
 
-ContentType = Union[
-    List["ContentBase"],
-    "ContentBase",
-    bool,
-    Dict[str, Any],
-    float,
-    int,
-    List[Any],
-    str,
-]
-
 
 def serialize(
     content: ContentType,
-    content_view: "ContentView",
+    content_view: ContentView,
     serialization_format: SerializationFormat,
 ) -> str:
     """Serialize a dataclass based on format and view.
@@ -88,7 +64,7 @@ def serialize(
 
 def serialize_write_file(
     content: ContentType,
-    content_view: "ContentView",
+    content_view: ContentView,
     file_mode: str,
     file: Path,
     serialization_format: SerializationFormat,
@@ -119,7 +95,7 @@ def serialize_write_file(
 
 def serialize_write_temp_file(
     content: ContentType,
-    content_view: "ContentView",
+    content_view: ContentView,
     serialization_format: SerializationFormat,
 ) -> Path:
     """Serialize and write content to a premanent temporary file.
@@ -171,7 +147,7 @@ def _prepare_content(
         return content
     if isinstance(content, (bool, dict, float, int, str)):
         return content
-    if is_dataclass(content):
+    if isinstance(content, ContentBase):
         return content.asdict(
             content_view=content_view,
             serialization_format=serialization_format,


Test patch:
diff --git a/tests/integration/_action_run_test.py b/tests/integration/_action_run_test.py
index dfb91d035..d8e6520a6 100644
--- a/tests/integration/_action_run_test.py
+++ b/tests/integration/_action_run_test.py
@@ -6,6 +6,7 @@
 
 from copy import deepcopy
 from typing import Any
+from typing import Callable
 from typing import Dict
 from typing import List
 from typing import Optional
@@ -15,8 +16,11 @@
 from ansible_navigator.app_public import AppPublic
 from ansible_navigator.configuration_subsystem import Constants as C
 from ansible_navigator.configuration_subsystem import NavigatorConfiguration
+from ansible_navigator.content_defs import ContentFormat
+from ansible_navigator.content_defs import ContentType
 from ansible_navigator.steps import Steps
 from ansible_navigator.ui_framework.ui import Action as Ui_action
+from ansible_navigator.ui_framework.ui import Form
 from ansible_navigator.ui_framework.ui import Interaction
 from ansible_navigator.ui_framework.ui import Ui
 
@@ -91,6 +95,29 @@ def callable_pass_one_arg(self, value=0):
     def callable_pass(self, **kwargs):
         """a do nothing callable"""
 
+    def content_format(
+        self,
+        value: Optional[ContentFormat] = None,
+        default: bool = False,
+    ) -> ContentFormat:
+        """A do nothing content format callable."""
+
+    def show(
+        self,
+        obj: ContentType,
+        content_format: Optional[ContentFormat] = None,
+        index: Optional[int] = None,
+        columns: Optional[List] = None,
+        await_input: bool = True,
+        filter_content_keys: Callable = lambda x: x,
+        color_menu_item: Callable = lambda *args, **kwargs: (0, 0),
+        content_heading: Callable = lambda *args, **kwargs: None,
+    ) -> "Interaction":
+        """me"""
+
+    def show_form(self, form: Form) -> Form:
+        """A do nothing show form callable."""
+
     def run_action_interactive(self) -> Any:
         """run the action
         The return type is set to Any here since not all actions
@@ -119,9 +146,10 @@ def run_action_interactive(self) -> Any:
             clear=self.callable_pass,
             menu_filter=self.callable_pass_one_arg,
             scroll=self.callable_pass_one_arg,
-            show=self.callable_pass,
+            show=self.show,
+            show_form=self.show_form,
             update_status=self.callable_pass,
-            serialization_format=self.callable_pass,
+            content_format=self.content_format,
         )
         match = re.match(self._app_action.Action.KEGEX, self._action_name)
         if not match:
diff --git a/tests/unit/ui_framework/test_colorize.py b/tests/unit/ui_framework/test_colorize.py
index b3fb8f6f4..a47868809 100644
--- a/tests/unit/ui_framework/test_colorize.py
+++ b/tests/unit/ui_framework/test_colorize.py
@@ -6,8 +6,8 @@
 from typing import NamedTuple
 from unittest.mock import patch
 
+from ansible_navigator.content_defs import ContentView
 from ansible_navigator.ui_framework.colorize import Colorize
-from ansible_navigator.ui_framework.content_defs import ContentView
 from ansible_navigator.utils.serialize import SerializationFormat
 from ansible_navigator.utils.serialize import serialize
 
diff --git a/tests/unit/ui_framework/test_progress_bar.py b/tests/unit/ui_framework/test_progress_bar.py
index 358d74656..4cecf0494 100644
--- a/tests/unit/ui_framework/test_progress_bar.py
+++ b/tests/unit/ui_framework/test_progress_bar.py
@@ -1,7 +1,7 @@
 """Test for the conversion of percent string to progress bars."""
 from dataclasses import dataclass
 
-from ansible_navigator.ui_framework.content_defs import ContentBase
+from ansible_navigator.content_defs import ContentBase
 from ansible_navigator.ui_framework.utils import convert_percentage
 
 
diff --git a/tests/unit/utils/test_serialize_dataclass.py b/tests/unit/utils/test_serialize_dataclass.py
index 02be89120..8e76521c0 100644
--- a/tests/unit/utils/test_serialize_dataclass.py
+++ b/tests/unit/utils/test_serialize_dataclass.py
@@ -13,8 +13,8 @@
 
 import pytest
 
-from ansible_navigator.ui_framework import ContentBase
-from ansible_navigator.ui_framework import ContentView
+from ansible_navigator.content_defs import ContentBase
+from ansible_navigator.content_defs import ContentView
 from ansible_navigator.utils.serialize import SerializationFormat
 from ansible_navigator.utils.serialize import serialize
 
diff --git a/tests/unit/utils/test_serialize_yaml.py b/tests/unit/utils/test_serialize_yaml.py
index 076dfb8d9..193e885dc 100644
--- a/tests/unit/utils/test_serialize_yaml.py
+++ b/tests/unit/utils/test_serialize_yaml.py
@@ -4,7 +4,7 @@
 
 from yaml import Dumper
 
-from ansible_navigator.ui_framework import ContentView
+from ansible_navigator.content_defs import ContentView
 from ansible_navigator.utils import serialize
 
 
diff --git a/tests/unit/utils/test_unserializable.py b/tests/unit/utils/test_unserializable.py
index b862f5db7..214e0cbed 100644
--- a/tests/unit/utils/test_unserializable.py
+++ b/tests/unit/utils/test_unserializable.py
@@ -3,7 +3,7 @@
 
 import pytest
 
-from ansible_navigator.ui_framework.content_defs import ContentView
+from ansible_navigator.content_defs import ContentView
 from ansible_navigator.utils.serialize import SerializationFormat
 from ansible_navigator.utils.serialize import serialize