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

Issue:
Format `{{ examples }}` and a `{{ readme }}`
- Added source.shell as it is used in some README files
- Added a test for `{{ readme }}`
- Updated the template action to set the format
- Added a new ContentFormat for pre-serialized YAML and a unit test.
- This also sets the file extension when `:open`ing a file
![image](https://user-images.githubusercontent.com/18386516/159707593-d6c29d57-c89e-4cb0-b3c3-3326e05b3881.png)



Discussion / hints:

Fix patch:
diff --git a/docs/changelog-fragments.d/1137.feature.md b/docs/changelog-fragments.d/1137.feature.md
new file mode 100644
index 000000000..d89108262
--- /dev/null
+++ b/docs/changelog-fragments.d/1137.feature.md
@@ -0,0 +1,4 @@
+Added ability to print in color for `settings --mode stdout`.
+This includes `settings --sample` and `settings --schema`.
+
+-- by {user}`cidrblock`
diff --git a/src/ansible_navigator/actions/settings.py b/src/ansible_navigator/actions/settings.py
index 2df94f3bb..430fd1c4c 100644
--- a/src/ansible_navigator/actions/settings.py
+++ b/src/ansible_navigator/actions/settings.py
@@ -1,7 +1,10 @@
 """The ``settings`` subcommand action."""
 
 from dataclasses import asdict
+from typing import Any
+from typing import Dict
 from typing import Tuple
+from typing import Union
 
 from ansible_navigator.configuration_subsystem.definitions import Constants
 from ..action_base import ActionBase
@@ -12,8 +15,7 @@
 from ..configuration_subsystem import to_presentable
 from ..configuration_subsystem import to_sample
 from ..configuration_subsystem import to_schema
-from ..content_defs import ContentView
-from ..content_defs import SerializationFormat
+from ..content_defs import ContentFormat
 from ..steps import StepType
 from ..steps import TypedStep
 from ..ui_framework import Color
@@ -22,7 +24,7 @@
 from ..ui_framework import CursesLines
 from ..ui_framework import Decoration
 from ..ui_framework import Interaction
-from ..utils.serialize import serialize
+from ..utils.print import print_to_stdout
 from . import _actions as actions
 from . import run_action
 
@@ -119,30 +121,26 @@ def run_stdout(self) -> RunStdoutReturn:
         :returns: RunStdoutReturn
         """
         self._logger.debug("settings requested in stdout mode")
+
+        content: Union[str, Dict[str, Any], PresentableSettingsEntries]
         if self._args.entry("settings_schema").value.source is not Constants.DEFAULT_CFG:
+            content = to_schema(self._args)
             if self._args.settings_schema == "json":
-                schema = to_schema(self._args)
-                print(schema)
-            return RunStdoutReturn(message="", return_code=0)
-
-        if self._args.settings_sample:
-            commented, _uncommented = to_sample(self._args)
-            print(commented)
-            return RunStdoutReturn(message="", return_code=0)
-
-        self._settings = to_presentable(self._args)
-        info_dump = serialize(
-            content=list(self._settings),
-            content_view=ContentView.NORMAL,
-            serialization_format=SerializationFormat.YAML,
-        )
-        if isinstance(info_dump, str):
-            print(info_dump)
-            return RunStdoutReturn(message="", return_code=0)
-        return RunStdoutReturn(
-            message="Settings could not be retrieved, please log an issue.",
-            return_code=1,
+                content_format = ContentFormat.JSON
+        elif self._args.settings_sample:
+            content, _uncommented = to_sample(self._args)
+            content_format = ContentFormat.YAML_TXT
+        else:
+            content = to_presentable(self._args)
+            content_format = ContentFormat.YAML
+
+        print_to_stdout(
+            content=content,
+            content_format=content_format,
+            share_directory=self._args.internals.share_directory,
+            use_color=self._args.display_color,
         )
+        return RunStdoutReturn(message="", return_code=0)
 
     def _build_main_menu(self) -> TypedStep:
         """Build the main menu of settings.
diff --git a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
index 0c2361cac..b66080c68 100644
--- a/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
+++ b/src/ansible_navigator/configuration_subsystem/navigator_configuration.py
@@ -267,7 +267,7 @@ class Internals:
             cli_parameters=CliParameters(short="--dc"),
             environment_variable_override="no_color",
             settings_file_path_override="color.enable",
-            short_description="Enable the use of color in the display",
+            short_description="Enable the use of color for mode interactive and stdout",
             value=SettingsEntryValue(default=True),
         ),
         SettingsEntry(
diff --git a/src/ansible_navigator/configuration_subsystem/transform.py b/src/ansible_navigator/configuration_subsystem/transform.py
index 9c0ff784f..3d1639f6b 100644
--- a/src/ansible_navigator/configuration_subsystem/transform.py
+++ b/src/ansible_navigator/configuration_subsystem/transform.py
@@ -3,14 +3,12 @@
 import json
 import textwrap
 
+from typing import Any
 from typing import Dict
 from typing import List
 from typing import Tuple
 
-from ..content_defs import ContentView
 from ..utils.compatibility import importlib_resources
-from ..utils.serialize import SerializationFormat
-from ..utils.serialize import serialize
 from .definitions import ApplicationConfiguration
 from .definitions import Constants
 from .definitions import SettingsEntry
@@ -49,7 +47,7 @@ def to_presentable(settings: ApplicationConfiguration) -> PresentableSettingsEnt
     return PresentableSettingsEntries(tuple(settings_list))
 
 
-def to_schema(settings: ApplicationConfiguration) -> str:
+def to_schema(settings: ApplicationConfiguration) -> Dict[str, Any]:
     """Build a json schema from the settings using the stub schema.
 
     :param settings: The application settings
@@ -71,7 +69,7 @@ def to_schema(settings: ApplicationConfiguration) -> str:
                 subschema = subschema.get(part, {}).get("properties")
         subschema[dot_parts[-1]]["description"] = entry.short_description
         if entry.choices:
-            subschema[dot_parts[-1]]["enum"] = entry.choices
+            subschema[dot_parts[-1]]["enum"] = list(entry.choices)  # may be a tuple
         if entry.value.default is not Constants.NOT_SET:
             subschema[dot_parts[-1]]["default"] = entry.value.default
 
@@ -83,11 +81,7 @@ def to_schema(settings: ApplicationConfiguration) -> str:
     partial_schema["version"] = version
     partial_schema["title"] = partial_schema["title"].format(version=version)
 
-    return serialize(
-        content=partial_schema,
-        content_view=ContentView.NORMAL,
-        serialization_format=SerializationFormat.JSON,
-    )
+    return partial_schema
 
 
 def to_sample(settings: ApplicationConfiguration) -> Tuple[str, str]:
diff --git a/src/ansible_navigator/utils/print.py b/src/ansible_navigator/utils/print.py
new file mode 100644
index 000000000..a800a0430
--- /dev/null
+++ b/src/ansible_navigator/utils/print.py
@@ -0,0 +1,117 @@
+"""Print pretty colors."""
+import math
+import os
+
+from typing import List
+
+from ansible_navigator.content_defs import ContentFormat
+from ansible_navigator.content_defs import ContentType
+from ansible_navigator.content_defs import ContentView
+from ansible_navigator.ui_framework.colorize import Colorize
+from ansible_navigator.ui_framework.colorize import rgb_to_ansi
+from ansible_navigator.ui_framework.curses_defs import SimpleLinePart
+from ansible_navigator.utils.serialize import serialize
+
+
+def color_bits() -> int:
+    """Determine to number of color bit the terminal can support.
+
+    :returns: The number of color bits
+    """
+    color_term = os.environ.get("COLORTERM", "").strip().lower()
+    if color_term in ("truecolor", "24bit"):
+        return 24
+    term = os.environ.get("TERM", "").strip().lower()
+    _term_name, _hyphen, colors = term.rpartition("-")
+    colors = colors.replace("color", "")
+    try:
+        return int(math.log2(int(colors)))
+    except (ValueError, TypeError):
+        return 4
+
+
+def color_lines(term_color_bits, tokenized) -> str:
+    """Transform tokenized lines to ANSI lines.
+
+    :param term_color_bits: The number of color bits the terminal supports
+    :param tokenized: The tokenized content
+    :returns: The ANSI string
+    """
+    lines = []
+    for line in tokenized:
+        printable = ""
+        for line_part in line:
+            color = line_part.color
+            if color is not None:
+                red, green, blue = color
+            if term_color_bits == 24:
+                if color is None:
+                    ansi_code = "\033[38;2;255;255;255m"
+                else:
+                    ansi_code = f"\033[38;2;{red};{green};{blue}m"
+            else:
+                if color is None:
+                    ansi_color = 1
+                else:
+                    number_of_colors = 2**term_color_bits
+                    ansi_color = rgb_to_ansi(red, green, blue, number_of_colors)
+                ansi_code = f"\033[38;5;{ansi_color}m"
+            printable += f"{ansi_code}{line_part.chars}\033[1m"
+        lines.append(printable)
+    return "".join(lines)
+
+
+def tokenize(
+    content_format: ContentFormat,
+    serialized: str,
+    share_directory: str,
+) -> List[List[SimpleLinePart]]:
+    """Serialize and tokenize an object.
+
+    :param content_format: The format type
+    :param serialized: The serialized content
+    :param share_directory: the current share directory
+    :returns: A list of list of line parts
+    """
+    colorizer = Colorize(
+        grammar_dir=os.path.join(share_directory, "grammar"),
+        theme_path=os.path.join(share_directory, "themes", "dark_vs.json"),
+    )
+
+    tokenized = colorizer.render(doc=serialized, scope=content_format.value.scope)
+    return tokenized
+
+
+def print_to_stdout(
+    content: ContentType,
+    content_format: ContentFormat,
+    share_directory: str,
+    use_color: bool,
+) -> None:
+    """Print some colored output to stdout.
+
+    :param content: The content to print out.
+    :param content_format: The content_format
+    :param share_directory: the current share directory
+    :param use_color: Indicates if color should be used
+    """
+    serialization_format = content_format.value.serialization
+    if serialization_format:
+        serialized = serialize(
+            content=content,
+            content_view=ContentView.NORMAL,
+            serialization_format=serialization_format,
+        )
+        output = serialized
+    else:
+        output = str(content)
+    if use_color:
+        tokenized = tokenize(
+            content_format=content_format,
+            serialized=output,
+            share_directory=share_directory,
+        )
+        terminal_color_bits = color_bits()
+        output = color_lines(terminal_color_bits, tokenized)
+
+    print(output)
diff --git a/src/ansible_navigator/utils/serialize.py b/src/ansible_navigator/utils/serialize.py
index 53f12bd3f..1ba20fc5d 100644
--- a/src/ansible_navigator/utils/serialize.py
+++ b/src/ansible_navigator/utils/serialize.py
@@ -145,7 +145,7 @@ def _prepare_content(
     content_view: ContentView,
     serialization_format: SerializationFormat,
 ) -> ContentType:
-    if isinstance(content, list):
+    if isinstance(content, (tuple, list)):
         if all(is_dataclass(c) for c in content):
             return [
                 c.asdict(


Test patch:
diff --git a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/2.json b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/2.json
index 5b41b8da7..28f82fd3f 100644
--- a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/2.json
+++ b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/2.json
@@ -10,7 +10,6 @@
         "compared_fixture": false
     },
     "output": [
-        "                        \"container-engine\": {",
         "                            \"default\": \"auto\",",
         "                            \"description\": \"Specify the container engine (auto=podman then docker)\",",
         "                            \"enum\": [",
diff --git a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/3.json b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/3.json
index afb49cc4e..76c99ff8e 100644
--- a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/3.json
+++ b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/3.json
@@ -10,7 +10,6 @@
         "compared_fixture": false
     },
     "output": [
-        "                        \"container-engine\": {",
         "                            \"default\": \"auto\",",
         "                            \"description\": \"Specify the container engine (auto=podman then docker)\",",
         "                            \"enum\": [",
diff --git a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/4.json b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/4.json
index 435a89256..b79fad7bf 100644
--- a/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/4.json
+++ b/tests/fixtures/integration/actions/settings/test_stdout_tmux.py/test/4.json
@@ -60,7 +60,7 @@
         "#   # The path to collection doc cache",
         "#   collection-doc-cache-path: $HOME/.cache/ansible-navigator/collection_doc_cache.db",
         "#   color:",
-        "#     # Enable the use of color in the display",
+        "#     # Enable the use of color for mode interactive and stdout",
         "#     enable: True",
         "#     # Enable or disable terminal color changing support with OSC 4",
         "#     osc4: True",
diff --git a/tests/unit/configuration_subsystem/test_json_schema.py b/tests/unit/configuration_subsystem/test_json_schema.py
index bda611e97..705f05499 100644
--- a/tests/unit/configuration_subsystem/test_json_schema.py
+++ b/tests/unit/configuration_subsystem/test_json_schema.py
@@ -1,6 +1,5 @@
 """Tests for the transformation of settings to a json schema."""
 
-import json
 
 from copy import deepcopy
 from pathlib import Path
@@ -25,8 +24,7 @@ def _schema_dict():
     settings = deepcopy(NavigatorConfiguration)
     settings.application_version = "test"
     schema = to_schema(settings)
-    as_dict = json.loads(schema)
-    return as_dict
+    return schema
 
 
 def test_basic(schema_dict: Dict[str, Any]):
diff --git a/tests/unit/utils/test_unserializable.py b/tests/unit/utils/test_unserializable.py
index 214e0cbed..d392d86e7 100644
--- a/tests/unit/utils/test_unserializable.py
+++ b/tests/unit/utils/test_unserializable.py
@@ -1,4 +1,5 @@
 """Tests for content that cannot be serialized."""
+from collections import deque
 from typing import Tuple
 
 import pytest
@@ -52,7 +53,7 @@ class CustomClass:
 
 @serialization_formats
 @content_views
-def test_tuple(
+def test_deque(
     content_view: Tuple[str, ContentView],
     serialization_format: Tuple[str, SerializationFormat],
 ):
@@ -63,7 +64,7 @@ def test_tuple(
     :param content_view: The content view
     :param serialization_format: The serialization format
     """
-    content = (1, 2, 3)
+    content = deque([1, 2, 3])
     serialized = serialize(
         content=content,  # type:ignore[arg-type]
         content_view=content_view[1],