Repository: ManimCommunity/manim

Issue:
Choice of preview command as configuration option
## Description of proposed feature

Currently, the preview command is fixed, e.g. `xdg-open` on Linux or similar `open` variant on other operating systems.

Even if these commands often can be configured to invoke a particular program, this is still limiting and assumes a GUI.

Having a `preview_command` on the config object  (or maybe even a command line option for easier discoverability) that is invoked if the configuration option is set would help making it easier to integrate `manim` in more workflows.

## How can the new feature be used?

   *  In my case, I'd like to invoke the  [`timg`](https://timg.sh) program that directly renders videos in the terminal.
   `xdg-open` could be configured to use that, but since `xdg-open` is assuming a GUI program to launch, it generates some blinking wait mouse-cursor as it awaits some GUI program to show up which never happens. Also it outputs some log messages that mess with the output on the terminal. So ideally, I'd like to directly invoke `timg`.
   With my little patch below, this is how that looks like (and it is actually pretty awesome to render things directly in the shell)
   ![Screenshot_20231118_194540](https://github.com/ManimCommunity/manim/assets/140937/946f137e-2d24-46fc-af78-cbd42e5668c5)

  * Or consider another use-case, in which a user would like to invoke a shell-script with the preview content to do something special with it, like displaying on a different screen etc.

## Additional comments

There is another thing that should probably be done: running the preview command in the foreground with `sp.run(commands)`, not `sp.Popen(commands)`. The reason why this is beneficial is that if the invoked program outputs something to the shell, it is not disturbed with any text output coming later, e.g. the shell prompt returning or other log messages.

The reason why this can be safely done without impacting the current functionality: the programs otherwise being invoked (`xdg-open`, `open`, etc.) _already_ go in the background, so there would be no change to that behavior.

Putting it all together, this is how I have currently patched two lines of my local copy of `manim` to do what I need to invoke `timg` that it can output on the terminal without disturbance of other programs messing with the terminal.

Here the program is hardcoded, but of course that should be be invoking `config["preview_command"]` in a proper implementation.

```patch
--- a/manim/utils/file_ops.py
+++ b/manim/utils/file_ops.py
@@ -192,7 +192,7 @@ def open_file(file_path, in_browser=False):
         os.startfile(file_path if not in_browser else file_path.parent)
     else:
         if current_os == "Linux":
-            commands = ["xdg-open"]
+            commands = ["timg"]    # This should be configurable
             file_path = file_path if not in_browser else file_path.parent
         elif current_os.startswith("CYGWIN"):
             commands = ["cygstart"]
@@ -205,7 +205,7 @@ def open_file(file_path, in_browser=False):
         else:
             raise OSError("Unable to identify your operating system...")
         commands.append(file_path)
-        sp.Popen(commands)
+        sp.run(commands)   # Running in foreground prevents messing with output
 
 
 def open_media_file(file_writer: SceneFileWriter) -> None:
```

If this would be a configuration option, then the part of the code would probably be something like
```Python
   if config["preview_command"]:
     #...
   elif current_os == "Linux":
     # ...
```

## Final thoughts
The suggestions (provide a command that is configurable, and run that command in the foreground instead of putting it in the background with Popen()) will be entirely backward compatible but will make it possible to give users a way to expand their workflows.


Discussion:
Thanks for the thorough investigation and auggestion, this is a good idea. 👍 
With the latest [1.5.3 release](https://github.com/hzeller/timg/releases/tag/v1.5.3) of [timg](http://timg.sh/), the rendering features of the terminal built into VSCode are also better supported, so that it is possible to directly render the generated preview-video in vscode if `timg` is chosen as preview command.

![manim-vscode](https://github.com/ManimCommunity/manim/assets/140937/6ec903cb-d03e-42e9-b9c1-66df221ac8a2)

That looks pretty neat as an idea
Should this wait until after the (planned) config improvements?

Resolving patch:
diff --git a/manim/_config/utils.py b/manim/_config/utils.py
--- a/manim/_config/utils.py
+++ b/manim/_config/utils.py
@@ -318,6 +318,7 @@ class MyScene(Scene): ...
         "zero_pad",
         "force_window",
         "no_latex_cleanup",
+        "preview_command",
     }
 
     def __init__(self) -> None:
@@ -767,6 +768,7 @@ def digest_args(self, args: argparse.Namespace) -> Self:
             "force_window",
             "dry_run",
             "no_latex_cleanup",
+            "preview_command",
         ]:
             if hasattr(args, key):
                 attr = getattr(args, key)
@@ -1016,6 +1018,14 @@ def no_latex_cleanup(self) -> bool:
     def no_latex_cleanup(self, value: bool) -> None:
         self._set_boolean("no_latex_cleanup", value)
 
+    @property
+    def preview_command(self) -> str:
+        return self._d["preview_command"]
+
+    @preview_command.setter
+    def preview_command(self, value: str) -> None:
+        self._set_str("preview_command", value)
+
     @property
     def verbosity(self) -> str:
         """Logger verbosity; "DEBUG", "INFO", "WARNING", "ERROR", or "CRITICAL" (-v)."""
diff --git a/manim/cli/render/global_options.py b/manim/cli/render/global_options.py
--- a/manim/cli/render/global_options.py
+++ b/manim/cli/render/global_options.py
@@ -105,4 +105,9 @@ def validate_gui_location(ctx, param, value):
         help="Prevents deletion of .aux, .dvi, and .log files produced by Tex and MathTex.",
         default=False,
     ),
+    option(
+        "--preview_command",
+        help="The command used to preview the output file (for example vlc for video files)",
+        default="",
+    ),
 )
diff --git a/manim/utils/file_ops.py b/manim/utils/file_ops.py
--- a/manim/utils/file_ops.py
+++ b/manim/utils/file_ops.py
@@ -32,7 +32,7 @@
 
 from manim import __version__, config, logger
 
-from .. import console
+from .. import config, console
 
 
 def is_mp4_format() -> bool:
@@ -204,8 +204,12 @@ def open_file(file_path, in_browser=False):
                 commands = ["open"] if not in_browser else ["open", "-R"]
         else:
             raise OSError("Unable to identify your operating system...")
+
+        # check after so that file path is set correctly
+        if config.preview_command:
+            commands = [config.preview_command]
         commands.append(file_path)
-        sp.Popen(commands)
+        sp.run(commands)
 
 
 def open_media_file(file_writer: SceneFileWriter) -> None:


Test patch: