Repository: ManimCommunity/manim

Issue:
Impossible to use preview with Scene().render()
## Description of bug / unexpected behavior
The title. For example : 
```python
class Test(ThreeDScene):
    def construct(self):
         # cool scene goes there

Test().render()
```
and a config file with `preview = True`. 
The config file is detected by manim, and the preview flag is introduced in config dict.

## Expected behavior
The scene should open in a preview windows as it normally does with `-p` flag in CLI


## How to reproduce the issue
See above.

## Additional comments
The preview logic is run in [main](https://github.com/huguesdevimeux/manim/blob/bf6c56e73a7a3132587c2c804d10c140d1929b92/manim/__main__.py#L49-L50), which is called in [__main__()](https://github.com/huguesdevimeux/manim/blob/bf6c56e73a7a3132587c2c804d10c140d1929b92/manim/__main__.py#L102-L103). 
The issue is when not using `manim`command, __main__ is not called. 

A potential fix would be to move this logic into the renderer. 



Discussion:
The `preview` config option in the config file is meant to change the behavior of the `-p` flag when called from the command line. If you are not using manim from the command line, it won't do anything, so this is expected behavior. (As a matter of fact, the original purpose of the `[CLI]` title of the config file was precisely to determine that the options contained therein will affect the command line, and not other parts of manim.)

Having said that, my opinion is that we shouldn't call an external program from the core library. My explanation is [here](https://github.com/ManimCommunity/manim/issues/829#issuecomment-739290741) cc @kolibril13 . I could be convinced otherwise if enough people feel this is an important feature.
It makes sense, 

> I could be in favor of implementing something like Scene.preview() or Scene.show(), but I honestly don't think that's necessary.

I believe this is an excellent idea, and can be useful for people don't use CLI, for some reason (as an example, I personnaly prefer running a scene directly from my IDE). Futhermore, PIL, matplotlib, etc have such a feature, it would make sense that manim has it to. 

I'll try to work on something like this.
Like I said, I'm not opposed to it, but I would be weary of feature creep. By this I mean that it would be fine to have a simple `show()` method, but I believe it should be kept simple and straightforward. Remember manim is not a plotting/video-showing library, it is a rendering library. (At least for now!)
I see. So, before going any further, I would like to have the opinion of @ManimCommunity/core : 
Here are the words of @leotrs :

> >   2. It is not yet possible to use the preview flag -p from the comment line, that is why I currently used matplotlib.
> 
> I am against implementing the preview flag in this way. The reason is that the preview flag has nothing to do with manim itself and it just asks another application to run a file created by manim. This is what the command line command is important: to interface between manim and other applications. I could be in favor of implementing something like Scene.preview() or Scene.show(), but I honestly don't think that's necessary.
> 
> EDIT: the reason why I don't think it's necessary is because implementing such a method would remove only two lines from the script you have posted. It makes much more sense to me to explicitly call matplotlib with two lines of code) than to have the core manim codebase do that for you.

What are your thoughts? Should the user be able to preview a scene directly from a python file (not from the CLI)? Or should one use like `Scene.preview()` ? 
IIRC, all the CLI `-p` option does internally is call the function `open_file_if_needed` right? Why not add a boolean `preview` keyword parameter to `Scene.render` which does exactly that?

On the topic of opening external applications via manim, we already sort of do this with `Mobject.show`, no? 
why not do a poll for this?..
> IIRC, all the CLI `-p` option does internally is call the function `open_file_if_needed` right? Why not add a boolean `preview` keyword parameter to `Scene.render` which does exactly that?
> 
> On the topic of opening external applications via manim, we already sort of do this with `Mobject.show`, no?

I would sooner remove Mobject.show than implement more functionality that depends on external applications.

I don't think that it is very unusual to ask the operating system to open a generated file with the corresponding default viewer. For MacOS and Windows, this should be covered by `open` or `start` -- and for UNIXoid systems that use a window managers which is a part of freedesktop.org (which is certainly all of the mainstream ones like Gnome and KDE), there is `xdg-open`. (Actually, we are already using said logic in `open_file` in `utils/file_ops.py`.)

I don't really think that when making a call to any of these, that we introduce a dependency on an external application (although I *can* relate to @leotrs's worries of feature creep). Given the popularity of the `-p` flag (judging from questions on Discord), I can see how people who like to render within the script would find a similar functionality for there useful.

Personally, I wouldn't mind adding a keyword argument to `render` which calls the existing `open_file` function.
To what benefit exactly? If it works and provides a convenience to users I don't see why we shouldn't. Calling render directly from the script may prove beneficial in debugging as well and in the case of manim this heavily involves viewing the output of a rendered scene, hence why I think the option of previews should not be limited to the command line.

I can get behind using xdg-open, but I would be _very much_ against using something like matplotlib (see [here](https://github.com/ManimCommunity/manim/issues/829)). And even then, if someone asks for a different application, or to add more features to the preview function, I would be very much against it.

But a simple clean call to xdg-open seems fine.

Resolving patch:
diff --git a/manim/__main__.py b/manim/__main__.py
--- a/manim/__main__.py
+++ b/manim/__main__.py
@@ -9,36 +9,8 @@
     get_scenes_to_render,
     scene_classes_from_file,
 )
-from manim.utils.file_ops import open_file as open_media_file
-from manim._config.main_utils import parse_args
-
-
-def open_file_if_needed(file_writer):
-    if config["verbosity"] != "DEBUG":
-        curr_stdout = sys.stdout
-        sys.stdout = open(os.devnull, "w")
-
-    open_file = any([config["preview"], config["show_in_file_browser"]])
 
-    if open_file:
-        file_paths = []
-
-        if config["save_last_frame"]:
-            file_paths.append(file_writer.image_file_path)
-        if config["write_to_movie"] and not config["save_as_gif"]:
-            file_paths.append(file_writer.movie_file_path)
-        if config["save_as_gif"]:
-            file_paths.append(file_writer.gif_file_path)
-
-        for file_path in file_paths:
-            if config["show_in_file_browser"]:
-                open_media_file(file_path, True)
-            if config["preview"]:
-                open_media_file(file_path, False)
-
-    if config["verbosity"] != "DEBUG":
-        sys.stdout.close()
-        sys.stdout = curr_stdout
+from manim._config.main_utils import parse_args
 
 
 def main():
@@ -102,7 +74,6 @@ def main():
                 try:
                     scene = SceneClass()
                     scene.render()
-                    open_file_if_needed(scene.renderer.file_writer)
                 except Exception:
                     console.print_exception()
 
diff --git a/manim/scene/scene.py b/manim/scene/scene.py
--- a/manim/scene/scene.py
+++ b/manim/scene/scene.py
@@ -28,6 +28,7 @@
 from ..renderer.cairo_renderer import CairoRenderer
 from ..utils.exceptions import EndSceneEarlyException
 from ..utils.family_ops import restructure_list_to_exclude_certain_family_members
+from ..utils.file_ops import open_media_file
 from ..utils.space_ops import rotate_vector
 
 
@@ -168,9 +169,14 @@ def __deepcopy__(self, clone_from_id):
                 result.mobject_updater_lists.append((mobject_clone, cloned_updaters))
         return result
 
-    def render(self):
+    def render(self, preview=False):
         """
-        Render this Scene.
+        Renders this Scene.
+
+        Parameters
+        ---------
+        preview : bool
+            If true, opens scene in a file viewer.
         """
         self.setup()
         try:
@@ -180,10 +186,18 @@ def render(self):
         self.tear_down()
         # We have to reset these settings in case of multiple renders.
         self.renderer.scene_finished(self)
+
         logger.info(
             f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations"
         )
 
+        # If preview open up the render after rendering.
+        if preview:
+            config["preview"] = True
+
+        if config["preview"] or config["show_in_file_browser"]:
+            open_media_file(self.renderer.file_writer)
+
     def setup(self):
         """
         This is meant to be implemented by any scenes which
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
@@ -13,6 +13,7 @@
 import platform
 import time
 import subprocess as sp
+from manim import config, logger
 from pathlib import Path
 
 from manim import __version__
@@ -77,3 +78,22 @@ def open_file(file_path, in_browser=False):
             raise OSError("Unable to identify your operating system...")
         commands.append(file_path)
         sp.Popen(commands)
+
+
+def open_media_file(file_writer):
+    file_paths = []
+
+    if config["save_last_frame"]:
+        file_paths.append(file_writer.image_file_path)
+    if config["write_to_movie"] and not config["save_as_gif"]:
+        file_paths.append(file_writer.movie_file_path)
+    if config["save_as_gif"]:
+        file_paths.append(file_writer.gif_file_path)
+
+    for file_path in file_paths:
+        if config["show_in_file_browser"]:
+            open_file(file_path, True)
+        if config["preview"]:
+            open_file(file_path, False)
+
+            logger.info(f"Previewed File at: {file_path}")


Test patch: