Repository: zulip/zulip-terminal (python)

Issue:
Improve user issue reporting - add copy feature to About popup
When reporting issues, it is useful to know the current version and platform information. This is already provided in the About popup, so it would be convenient to add a feature to copy that information to the clipboard for use in bug (or feature) reports.

The code already wraps copy functionality, so the primary aspect here is to construct the information appropriately from the data available in the About popup.

We expect to standardize on <kbd>c</kbd> for copying, though the command string is somewhat in flux at present. See for example `COPY_STREAM_EMAIL`.

This was previously discussed in `#zulip-terminal>View object has no attribute topic_w` on chat.zulip.org.

Pull request description:
Functionality to copy information from the about popup
### What does this PR do, and why?
Adds hotkey 'c' to copy information from the about popup. It's copied in this format:

```
#### Application
Zulip Terminal: 0.7.0+git

#### Server
Version: 9.0-dev-3226-gbc50a9743b
Feature level: 265

#### Application Configuration
Theme: zt_dark
Autohide: disabled
Maximum footlinks: 3
Color depth: 256
Notifications: disabled
Exit confirmation: enabled

#### Detected Environment
Platform: WSL
Python: 3.10.12 (CPython) 
```

### External discussion & connections
<!-- [x] all that apply, specifying topic and adding numbers after # for issues/PRs -->
- [x] Discussed in **#zulip-terminal** in `Copy Information from AboutPopup #T1493`
- [x] Fully fixes #1493
- [ ] Partially fixes issue #
- [ ] Builds upon previous unmerged work in PR #
- [ ] Is a follow-up to work in PR #
- [ ] Requires merge of PR #
- [ ] Merge will enable work on #

### How did you test this?
<!-- [x] all that apply -->
- [x] Manually - Behavioral changes
- [x] Manually - Visual changes
- [x] Adapting existing automated tests
- [x] Adding automated tests for new behavior (or missing tests)
- [ ] Existing automated tests should already cover this (*only a refactor of tested code*)

### Self-review checklist for each commit
- [x] It is a [minimal coherent idea](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development)
- [x] It has a commit summary following the [documented style](https://github.com/zulip/zulip-terminal#structuring-commits---speeding-up-reviews-merging--development) (title & body)
- [ ] It has a commit summary describing the  motivation and reasoning for the change
- [x] It individually passes linting and tests
- [x] It contains test additions for any new behavior
- [x] It flows clearly from a previous branch commit, and/or prepares for the next commit

### Visual changes    <!-- DELETE SECTION IF NO VISUAL CHANGE -->
<!-- Zulip tips at https://zulip.readthedocs.io/en/latest/tutorials/screenshot-and-gif-software.html -->
<!-- For video, try asciinema; after uploading, embed using
[![yourtitle](https://asciinema.org/a/<id>.png)](https://asciinema.org/a/<id>)
-->
<!-- NOTE: Attached videos/images will be clearer from smaller terminal windows -->
| Before | After |
|--------|--------|
| ![image](https://github.com/zulip/zulip-terminal/assets/110327345/75fb979f-32dc-4818-b9bc-a25515cb48fe)| ![image](https://github.com/zulip/zulip-terminal/assets/110327345/6c37d8db-0743-4a50-a97e-1d036f61bdc6)|

Patch:
diff --git a/docs/hotkeys.md b/docs/hotkeys.md
index 1c9e4bf..0d1ef3a 100644
--- a/docs/hotkeys.md
+++ b/docs/hotkeys.md
@@ -10,6 +10,7 @@
 |Show/hide About Menu|<kbd>Meta</kbd> + <kbd>?</kbd>|
 |Go back|<kbd>Esc</kbd>|
 |Open draft message saved in this session|<kbd>d</kbd>|
+|Copy information from About Menu to clipboard|<kbd>c</kbd>|
 |Redraw screen|<kbd>Ctrl</kbd> + <kbd>l</kbd>|
 |Quit|<kbd>Ctrl</kbd> + <kbd>c</kbd>|
 |Show/hide user information (from users list)|<kbd>i</kbd>|
diff --git a/zulipterminal/config/keys.py b/zulipterminal/config/keys.py
index 473d19c..e8d415e 100644
--- a/zulipterminal/config/keys.py
+++ b/zulipterminal/config/keys.py
@@ -56,6 +56,11 @@ KEY_BINDINGS: Dict[str, KeyBinding] = {
         'help_text': 'Open draft message saved in this session',
         'key_category': 'general',
     },
+    'COPY_ABOUT_INFO': {
+        'keys': ['c'],
+        'help_text': 'Copy information from About Menu to clipboard',
+        'key_category': 'general',
+    },
     'GO_UP': {
         'keys': ['up', 'k'],
         'help_text': 'Go up / Previous message',
diff --git a/zulipterminal/ui_tools/views.py b/zulipterminal/ui_tools/views.py
index 30fcc42..068ebad 100644
--- a/zulipterminal/ui_tools/views.py
+++ b/zulipterminal/ui_tools/views.py
@@ -1097,6 +1097,7 @@ class AboutView(PopUpView):
             if server_feature_level
             else []
         )
+
         contents = [
             ("Application", [("Zulip Terminal", zt_version)]),
             ("Server", [("Version", server_version)] + self.feature_level_content),
@@ -1115,16 +1116,33 @@ class AboutView(PopUpView):
                 ],
             ),
             (
-                "Detected environment",
+                "Detected Environment",
                 [("Platform", PLATFORM), ("Python", detected_python_in_full())],
             ),
         ]
 
+        # Prepare string version of contents to support copying to clipboard
+        sections = []
+        for section_title, properties in contents:
+            formatted_properties = "\n".join(
+                f"{label}: {value}" for label, value in properties
+            )
+            sections.append(f"#### {section_title}\n{formatted_properties}")
+        self.copy_info = "\n\n".join(sections)
+
+        about_keys = "[" + ", ".join(display_keys_for_command("COPY_ABOUT_INFO")) + "]"
+        contents.append((f"Copy information to clipboard {about_keys}", []))
+
         popup_width, column_widths = self.calculate_table_widths(contents, len(title))
         widgets = self.make_table_with_categories(contents, column_widths)
 
         super().__init__(controller, widgets, "ABOUT", popup_width, title)
 
+    def keypress(self, size: urwid_Size, key: str) -> str:
+        if is_command_key("COPY_ABOUT_INFO", key):
+            self.controller.copy_to_clipboard(self.copy_info, "About info")
+        return super().keypress(size, key)
+
 
 class UserInfoView(PopUpView):
     def __init__(self, controller: Any, user_id: int, title: str, command: str) -> None:


Test patch:
diff --git a/tests/ui_tools/test_popups.py b/tests/ui_tools/test_popups.py
index cb26395..0c98808 100644
--- a/tests/ui_tools/test_popups.py
+++ b/tests/ui_tools/test_popups.py
@@ -189,6 +189,12 @@ class TestAboutView:
         )
         server_version, server_feature_level = MINIMUM_SUPPORTED_SERVER_VERSION
 
+        # FIXME: Since we don't test on WSL explicitly, for now
+        #        treat PLATFORM as WSL in order for it to be supported
+        mocker.patch(MODULE + ".PLATFORM", "WSL")
+
+        mocker.patch(MODULE + ".detected_python_in_full", lambda: "[Python version]")
+
         self.about_view = AboutView(
             self.controller,
             "About",
@@ -213,6 +219,14 @@ class TestAboutView:
         self.about_view.keypress(size, key)
         assert self.controller.exit_popup.called
 
+    @pytest.mark.parametrize("key", {*keys_for_command("COPY_ABOUT_INFO")})
+    def test_keypress_copy_info(
+        self, key: str, widget_size: Callable[[Widget], urwid_Size]
+    ) -> None:
+        size = widget_size(self.about_view)
+        self.about_view.keypress(size, key)
+        assert self.controller.copy_to_clipboard.called
+
     def test_keypress_exit_popup_invalid_key(
         self, widget_size: Callable[[Widget], urwid_Size]
     ) -> None:
@@ -261,9 +275,30 @@ class TestAboutView:
             "Application",
             "Server",
             "Application Configuration",
-            "Detected environment",
+            "Detected Environment",
+            "Copy information to clipboard [c]",
         ]
 
+    def test_copied_content(self) -> None:
+        expected_output = f"""#### Application
+Zulip Terminal: {ZT_VERSION}
+
+#### Server
+Version: {MINIMUM_SUPPORTED_SERVER_VERSION[0]}
+
+#### Application Configuration
+Theme: zt_dark
+Autohide: disabled
+Maximum footlinks: 3
+Color depth: 256
+Notifications: disabled
+Exit confirmation: disabled
+
+#### Detected Environment
+Platform: WSL
+Python: [Python version]"""
+        assert self.about_view.copy_info == expected_output
+
 
 class TestUserInfoView:
     @pytest.fixture(autouse=True)