diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 44c7128..3ea51b4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -10,8 +10,8 @@ repos:
         language: python
         files: \.rst$
         require_serial: true
-  - repo: https://github.com/psf/black.git
-    rev: 23.7.0
+  - repo: https://github.com/psf/black-pre-commit-mirror
+    rev: 23.9.1
     hooks:
       - id: black
         language_version: python3
diff --git a/cookiecutter/prompt.py b/cookiecutter/prompt.py
index f5c2824..16830d4 100644
--- a/cookiecutter/prompt.py
+++ b/cookiecutter/prompt.py
@@ -20,7 +20,13 @@ def read_user_variable(var_name, default_value, prompts=None, prefix=""):
         if prompts and var_name in prompts.keys() and prompts[var_name]
         else var_name
     )
-    return Prompt.ask(f"{prefix}{question}", default=default_value)
+
+    while True:
+        variable = Prompt.ask(f"{prefix}{question}", default=default_value)
+        if variable is not None:
+            break
+
+    return variable
 
 
 class YesNoPrompt(Confirm):
@@ -243,8 +249,10 @@ def prompt_for_config(context, no_input=False):
     # values might refer to them.
 
     count = 0
-    size = len(context['cookiecutter'].items())
-    for key, raw in context['cookiecutter'].items():
+    all_prompts = context['cookiecutter'].items()
+    visible_prompts = [k for k, _ in all_prompts if not k.startswith("_")]
+    size = len(visible_prompts)
+    for key, raw in all_prompts:
         if key.startswith('_') and not key.startswith('__'):
             cookiecutter_dict[key] = raw
             continue
diff --git a/docs/advanced/human_readable_prompts.rst b/docs/advanced/human_readable_prompts.rst
index ee30e12..51ebc92 100644
--- a/docs/advanced/human_readable_prompts.rst
+++ b/docs/advanced/human_readable_prompts.rst
@@ -22,15 +22,15 @@ See the following cookiecutter config as example:
         "init_git": true,
         "linting": ["ruff", "flake8", "none"],
         "__prompts__": {
-            "package_name": "Select your package name:",
-            "module_name": "Select your module name:",
-            "package_name_stylized": "Stylized package name:",
-            "short_description": "Short description:",
-            "github_username": "GitHub username or organization:",
-            "full_name": "Author full name:",
-            "email": "Author email:",
-            "command_line_interface": "Add CLI:",
-            "init_git": "Initialize a git repository:",
+            "package_name": "Select your package name",
+            "module_name": "Select your module name",
+            "package_name_stylized": "Stylized package name",
+            "short_description": "Short description",
+            "github_username": "GitHub username or organization",
+            "full_name": "Author full name",
+            "email": "Author email",
+            "command_line_interface": "Add CLI",
+            "init_git": "Initialize a git repository",
             "linting": {
                 "__prompt__": "Which linting tool do you want to use?",
                 "ruff": "Ruff",
diff --git a/docs/installation.rst b/docs/installation.rst
index 995fd07..00e8d5f 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -104,6 +104,12 @@ Alternate installations
 
     brew install cookiecutter
 
+**Void Linux:**
+
+.. code-block:: bash
+
+    xbps-install cookiecutter
+
 **Pipx (Linux, OSX and Windows):**
 
 .. code-block:: bash
diff --git a/docs/tutorials/tutorial2.rst b/docs/tutorials/tutorial2.rst
index d16f6b7..0a93191 100644
--- a/docs/tutorials/tutorial2.rst
+++ b/docs/tutorials/tutorial2.rst
@@ -66,7 +66,7 @@ By running following command `cookiecutter.zip` will get generated which can be
 .. code-block:: bash
 
    $ (SOURCE_DIR=$(basename $PWD) ZIP=cookiecutter.zip && # Set variables
-   pushd && # Set parent directory as working directory
+   pushd .. && # Set parent directory as working directory
    zip -r $ZIP $SOURCE_DIR --exclude $SOURCE_DIR/$ZIP --quiet && # ZIP cookiecutter
    mv $ZIP $SOURCE_DIR/$ZIP && # Move ZIP to original directory
    popd && # Restore original work directory

diff --git a/tests/test_read_user_variable.py b/tests/test_read_user_variable.py
index 02890a1..6fac4d3 100644
--- a/tests/test_read_user_variable.py
+++ b/tests/test_read_user_variable.py
@@ -1,18 +1,36 @@
 """test_read_user_variable."""
+import pytest
 from cookiecutter.prompt import read_user_variable
 
 VARIABLE = 'project_name'
 DEFAULT = 'Kivy Project'
 
 
-def test_click_invocation(mocker):
+@pytest.fixture
+def mock_prompt(mocker):
+    """Return a mocked version of the 'Prompt.ask' function."""
+    return mocker.patch('rich.prompt.Prompt.ask')
+
+
+def test_click_invocation(mock_prompt):
     """Test click function called correctly by cookiecutter.
 
     Test for string type invocation.
     """
-    prompt = mocker.patch('rich.prompt.Prompt.ask')
-    prompt.return_value = DEFAULT
+    mock_prompt.return_value = DEFAULT
 
     assert read_user_variable(VARIABLE, DEFAULT) == DEFAULT
 
-    prompt.assert_called_once_with(VARIABLE, default=DEFAULT)
+    mock_prompt.assert_called_once_with(VARIABLE, default=DEFAULT)
+
+
+def test_input_loop_with_null_default_value(mock_prompt):
+    """Test `Prompt.ask` is run repeatedly until a valid answer is provided.
+
+    Test for `default_value` parameter equal to None.
+    """
+    # Simulate user providing None input initially and then a valid input
+    mock_prompt.side_effect = [None, DEFAULT]
+
+    assert read_user_variable(VARIABLE, None) == DEFAULT
+    assert mock_prompt.call_count == 2

Variable with null default no longer being required
* Cookiecutter version: 2.3.0
* Template project url: internal
* Python version: Python 3.8.12
* Operating System: Debian GNU/Linux 11

### Description:

After updating from 2.1.1 to 2.3.0, setting a field's default value in `cookiecutter.json` to `null` no longer makes that field required. It instead passes `None` as the value of that variable. Previously, it would repeat that prompt until you gave it a value.

It looks like this happened in the switch from `click` to `rich` in 2.3.0 - `click` doesn't allow a prompt without a default value to be empty (there's [a `while True` loop](https://github.com/pallets/click/blob/388b0e14093355e30b17ffaff0c7588533d9dbc8/src/click/termui.py#L163)), while `rich` does ([it just casts the input and makes sure it doesn't cause a ValueError](https://github.com/Textualize/rich/blob/720800e6930d85ad027b1e9bd0cbb96b5e994ce3/rich/prompt.py#L230) -- which it doesn't, as `str(None)=="None"`). It seems like, with `rich`, you have to handle that logic yourself.

### What I've run:

An example (`component_name`'s value is `null`, `service_name`'s is `test`):

```
Template 1:

  [1/8] component_name: 
  [2/8] service_name (test): ^C
Aborted!
```

Compare to output with version 2.1.1:

```
Template 1:

component_name: 
component_name: 
component_name: test
service_name [test]: ^CAborted!
```
tests/test_read_user_variable.py::test_input_loop_with_null_default_value
tests/test_read_user_variable.py::test_click_invocation