diff --git a/example_scenes/manim.cfg b/example_scenes/manim.cfg new file mode 100644 index 0000000000..a11d17e5bf --- /dev/null +++ b/example_scenes/manim.cfg @@ -0,0 +1,15 @@ + +#This is some custom configuration just as an example +[logger] +logging_keyword = magenta +logging_level_notset = dim +logging_level_debug = yellow +logging_level_info = dim purple +logging_level_warning = dim red +logging_level_error = red +logging_level_critical = red +log_level = +log_time = dim yellow +log_message = green +log_path = dim blue + diff --git a/manim/__main__.py b/manim/__main__.py index 63f7bebba4..61d4b987e5 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -12,7 +12,7 @@ from .utils.sounds import play_error_sound from .utils.sounds import play_finish_sound from . import constants -from .logger import logger +from .logger import logger,console def open_file_if_needed(file_writer): @@ -72,10 +72,10 @@ def prompt_user_for_choice(scene_classes): for count, scene_class in enumerate(scene_classes): count += 1 # start with 1 instead of 0 name = scene_class.__name__ - print("%d: %s" % (count, name)) + console.print(f"{count}: {name}", style="logging.level.info") num_to_class[count] = scene_class try: - user_input = input(constants.CHOOSE_NUMBER_MESSAGE) + user_input = console.input(f"[log.message] {constants.CHOOSE_NUMBER_MESSAGE} [/log.message]") return [num_to_class[int(num_str)] for num_str in re.split(r"\s*,\s*", user_input.strip())] except KeyError: diff --git a/manim/config.py b/manim/config.py index 97ba59443a..74e6ab7939 100644 --- a/manim/config.py +++ b/manim/config.py @@ -1,20 +1,19 @@ """ config.py --------- - Process the manim.cfg file and the command line arguments into a single config object. - """ - import os import sys -import argparse -import configparser + import colour -from .utils.tex import TexTemplateFromFile, TexTemplate -from .logger import logger + from . import constants +from .utils.config_utils import _run_config, _init_dirs, _from_command_line + +from .logger import logger +from .utils.tex import TexTemplate, TexTemplateFromFile __all__ = ["file_writer_config", "config", "camera_config"] @@ -22,17 +21,17 @@ def _parse_config(config_parser, args): """Parse config files and CLI arguments into a single dictionary.""" # By default, use the CLI section of the digested .cfg files - default = config_parser['CLI'] + default = config_parser["CLI"] # Handle the *_quality flags. These determine the section to read # and are stored in 'camera_config'. Note the highest resolution # passed as argument will be used. - for flag in ['fourk_quality', 'high_quality', 'medium_quality', 'low_quality']: + for flag in ["fourk_quality", "high_quality", "medium_quality", "low_quality"]: if getattr(args, flag): section = config_parser[flag] break else: - section = config_parser['CLI'] + section = config_parser["CLI"] config = {opt: section.getint(opt) for opt in config_parser[flag]} # The -r, --resolution flag overrides the *_quality flags @@ -42,37 +41,34 @@ def _parse_config(config_parser, args): height, width = int(height_str), int(width_str) else: height, width = int(args.resolution), int(16 * height / 9) - config['camera_config'].update({'pixel_height': height, - 'pixel_width': width}) + config["camera_config"].update({"pixel_height": height, "pixel_width": width}) # Handle the -c (--color) flag if args.color is not None: try: background_color = colour.Color(args.color) except AttributeError as err: - logger.warning('Please use a valid color.') + logger.warning("Please use a valid color.") logger.error(err) sys.exit(2) else: - background_color = colour.Color(default['background_color']) - config['background_color'] = background_color + background_color = colour.Color(default["background_color"]) + config["background_color"] = background_color # Set the rest of the frame properties - config['frame_height'] = 8.0 - config['frame_width'] = (config['frame_height'] - * config['pixel_width'] - / config['pixel_height']) - config['frame_y_radius'] = config['frame_height'] / 2 - config['frame_x_radius'] = config['frame_width'] / 2 - config['top'] = config['frame_y_radius'] * constants.UP - config['bottom'] = config['frame_y_radius'] * constants.DOWN - config['left_side'] = config['frame_x_radius'] * constants.LEFT - config['right_side'] = config['frame_x_radius'] * constants.RIGHT + config["frame_height"] = 8.0 + config["frame_width"] = ( + config["frame_height"] * config["pixel_width"] / config["pixel_height"] + ) + config["frame_y_radius"] = config["frame_height"] / 2 + config["frame_x_radius"] = config["frame_width"] / 2 + config["top"] = config["frame_y_radius"] * constants.UP + config["bottom"] = config["frame_y_radius"] * constants.DOWN + config["left_side"] = config["frame_x_radius"] * constants.LEFT + config["right_side"] = config["frame_x_radius"] * constants.RIGHT # Handle the --tex_template flag. Note we accept None if the flag is absent - filename = (os.path.expanduser(args.tex_template) - if args.tex_template is not None - else None) + filename = os.path.expanduser(args.tex_template) if args.tex_template else None if filename is not None and not os.access(filename, os.R_OK): # custom template not available, fallback to default @@ -81,346 +77,19 @@ def _parse_config(config_parser, args): "Falling back to the default template." ) filename = None - config['tex_template_file'] = filename - config['tex_template'] = (TexTemplateFromFile(filename=filename) - if filename is not None - else TexTemplate()) - - return config - - -def _parse_file_writer_config(config_parser, args): - """Parse config files and CLI arguments into a single dictionary.""" - # By default, use the CLI section of the digested .cfg files - default = config_parser['CLI'] - - # This will be the final file_writer_config dict exposed to the user - fw_config = {} - - # Handle input files and scenes. Note these cannot be set from - # the .cfg files, only from CLI arguments - fw_config['input_file'] = args.file - fw_config['scene_names'] = (args.scene_names - if args.scene_names is not None else []) - fw_config['output_file'] = args.output_file - - # Handle all options that are directly overridden by CLI - # arguments. Note ConfigParser options are all strings and each - # needs to be converted to the appropriate type. Thus, we do this - # in batches, depending on their type: booleans and strings - for boolean_opt in ['preview', 'show_file_in_finder', 'quiet', 'sound', - 'leave_progress_bars', 'write_to_movie', 'save_last_frame', - 'save_pngs', 'save_as_gif', 'write_all']: - attr = getattr(args, boolean_opt) - fw_config[boolean_opt] = (default.getboolean(boolean_opt) - if attr is None else attr) - # for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: - for str_opt in ['media_dir']: - attr = getattr(args, str_opt) - fw_config[str_opt] = (default[str_opt] if attr is None else attr) - dir_names = {'video_dir': 'videos', - 'tex_dir': 'Tex', - 'text_dir': 'texts'} - for name in dir_names: - fw_config[name] = os.path.join(fw_config['media_dir'], dir_names[name]) - - # Handle the -s (--save_last_frame) flag: invalidate the -w flag - # At this point the save_last_frame option has already been set by - # both CLI and the cfg file, so read the config dict directly - if fw_config['save_last_frame']: - fw_config['write_to_movie'] = False - - # Handle the -t (--transparent) flag. This flag determines which - # section to use from the .cfg file. - section = config_parser['transparent'] if args.transparent else default - for opt in ['png_mode', 'movie_file_extension', 'background_opacity']: - fw_config[opt] = section[opt] - - # Handle the -n flag. Read first from the cfg and then override with CLI. - # These two are integers -- use getint() - for opt in ['from_animation_number', 'upto_animation_number']: - fw_config[opt] = default.getint(opt) - if fw_config['upto_animation_number'] == -1: - fw_config['upto_animation_number'] = float('inf') - nflag = args.from_animation_number - if nflag is not None: - if ',' in nflag: - start, end = nflag.split(',') - fw_config['from_animation_number'] = int(start) - fw_config['upto_animation_number'] = int(end) - else: - fw_config['from_animation_number'] = int(nflag) - - # Handle the --dry_run flag. This flag determines which section - # to use from the .cfg file. All options involved are boolean. - # Note this overrides the flags -w, -s, -a, -g, and -i. - if args.dry_run: - for opt in ['write_to_movie', 'save_last_frame', 'save_pngs', - 'save_as_gif', 'write_all']: - fw_config[opt] = config_parser['dry_run'].getboolean(opt) - - # Read in the streaming section -- all values are strings - fw_config['streaming'] = {opt: config_parser['streaming'][opt] - for opt in ['live_stream_name', 'twitch_stream_key', - 'streaming_protocol', 'streaming_ip', - 'streaming_protocol', 'streaming_client', - 'streaming_port', 'streaming_port', - 'streaming_console_banner']} - - # For internal use (no CLI flag) - fw_config['skip_animations'] = any([fw_config['save_last_frame'], - fw_config['from_animation_number']]) - - return fw_config - - -def _parse_cli(arg_list, input=True): - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description='Animation engine for explanatory math videos', - epilog=f"Made with {'<3' if os.name == 'nt' else '❤ '} by the manim community devs" - ) - if input: - parser.add_argument( - "file", - help="path to file holding the python code for the scene", - ) - parser.add_argument( - "scene_names", - nargs="*", - help="Name of the Scene class you want to see", - default=[''], - ) - parser.add_argument( - "-o", "--output_file", - help="Specify the name of the output file, if " - "it should be different from the scene class name", - default='', - ) - - # The following use (action='store_const', const=True) instead of - # the built-in (action='store_true'). This is because the latter - # will default to False if not specified, while the former sets no - # default value. Since we want to set the default value in - # manim.cfg rather than here, we use the former. - parser.add_argument( - "-p", "--preview", - action="store_const", - const=True, - help="Automatically open the saved file once its done", - ) - parser.add_argument( - "-f", "--show_file_in_finder", - action="store_const", - const=True, - help="Show the output file in finder", - ) - parser.add_argument( - "-q", "--quiet", - action="store_const", - const=True, - help="Quiet mode", - ) - parser.add_argument( - "--sound", - action="store_const", - const=True, - help="Play a success/failure sound", - ) - parser.add_argument( - "--leave_progress_bars", - action="store_const", - const=True, - help="Leave progress bars displayed in terminal", - ) - parser.add_argument( - "-a", "--write_all", - action="store_const", - const=True, - help="Write all the scenes from a file", - ) - parser.add_argument( - "-w", "--write_to_movie", - action="store_const", - const=True, - help="Render the scene as a movie file", - ) - parser.add_argument( - "-s", "--save_last_frame", - action="store_const", - const=True, - help="Save the last frame (and do not save movie)", - ) - parser.add_argument( - "-g", "--save_pngs", - action="store_const", - const=True, - help="Save each frame as a png", - ) - parser.add_argument( - "-i", "--save_as_gif", - action="store_const", - const=True, - help="Save the video as gif", - ) - - # The default value of the following is set in manim.cfg - parser.add_argument( - "-c", "--color", - help="Background color", - ) - parser.add_argument( - "--background_opacity", - help="Background opacity", - ) - parser.add_argument( - "--media_dir", - help="directory to write media", - ) - # video_group = parser.add_mutually_exclusive_group() - # video_group.add_argument( - # "--video_dir", - # help="directory to write file tree for video", - # ) - # parser.add_argument( - # "--tex_dir", - # help="directory to write tex", - # ) - # parser.add_argument( - # "--text_dir", - # help="directory to write text", - # ) - parser.add_argument( - "--tex_template", - help="Specify a custom TeX template file", - ) - - # All of the following use (action="store_true"). This means that - # they are by default False. In contrast to the previous ones that - # used (action="store_const", const=True), the following do not - # correspond to a single configuration option. Rather, they - # override several options at the same time. - - # The following overrides -w, -a, -g, and -i - parser.add_argument( - "--dry_run", - action="store_true", - help="Do a dry run (render scenes but generate no output files)", - ) - - # The following overrides PNG_MODE, MOVIE_FILE_EXTENSION, and - # BACKGROUND_OPACITY - parser.add_argument( - "-t", "--transparent", - action="store_true", - help="Render to a movie file with an alpha channel", - ) - - # The following are mutually exclusive and each overrides - # FRAME_RATE, PIXEL_HEIGHT, and PIXEL_WIDTH, - parser.add_argument( - "-l", "--low_quality", - action="store_true", - help="Render at low quality (for fastest rendering)", - ) - parser.add_argument( - "-m", "--medium_quality", - action="store_true", - help="Render at medium quality (for much faster rendering)", - ) - parser.add_argument( - "-e", "--high_quality", - action="store_true", - help="Render at high quality (for slightly faster rendering)", - ) - parser.add_argument( - "-k", "--fourk_quality", - action="store_true", - help="Render at 4K quality (slower rendering)", - ) - - # This overrides any of the above - parser.add_argument( - "-r", "--resolution", - help="Resolution, passed as \"height,width\"", + config["tex_template_file"] = filename + config["tex_template"] = ( + TexTemplateFromFile(filename=filename) + if filename is not None + else TexTemplate() ) - # This sets FROM_ANIMATION_NUMBER and UPTO_ANIMATION_NUMBER - parser.add_argument( - "-n", "--from_animation_number", - help="Start rendering not from the first animation, but" - "from another, specified by its index. If you pass" - "in two comma separated values, e.g. \"3,6\", it will end" - "the rendering at the second value", - ) - - # Specify the manim.cfg file - parser.add_argument( - "--config_file", - help="Specify the configuration file", - ) - - return parser.parse_args(arg_list) - - -def _init_dirs(config): - # Make sure all folders exist - for folder in [config["media_dir"], config["video_dir"], - config["tex_dir"], config["text_dir"]]: - if not os.path.exists(folder): - os.makedirs(folder) - - -def _from_command_line(): - """Determine if manim was called from the command line.""" - # Manim can be called from the command line in three different - # ways. The first two involve using the manim or manimcm commands - prog = os.path.split(sys.argv[0])[-1] - from_cli_command = prog in ['manim', 'manimcm'] - - # The third way involves using `python -m manim ...`. In this - # case, the CLI arguments passed to manim do not include 'manim', - # 'manimcm', or even 'python'. However, the -m flag will always - # be the first argument. - from_python_m = sys.argv[0] == '-m' - - return from_cli_command or from_python_m - + return config -# Config files to be parsed, in ascending priority -library_wide = os.path.join(os.path.dirname(__file__), 'default.cfg') -config_files = [ - library_wide, - os.path.expanduser('~/.manim.cfg'), -] +args, config_parser, file_writer_config, successfully_read_files = _run_config() if _from_command_line(): - args = _parse_cli(sys.argv[1:]) - if args.config_file is not None: - if os.path.exists(args.config_file): - config_files.append(args.config_file) - else: - raise FileNotFoundError(f"Config file {args.config_file} doesn't exist") - else: - script_directory_file_config = os.path.join(os.path.dirname(args.file), 'manim.cfg') - if os.path.exists(script_directory_file_config): - config_files.append(script_directory_file_config) - -else: - # In this case, we still need an empty args object. - args = _parse_cli([], input=False) - # Need to populate the options left out - args.file, args.scene_names, args.output_file = '', '', '' - -config_parser = configparser.ConfigParser() -successfully_read_files = config_parser.read(config_files) -logger.info(f'Read configuration files: {successfully_read_files}') - -# this is for internal use when writing output files -file_writer_config = _parse_file_writer_config(config_parser, args) - -# this is for the user + logger.info(f"Read configuration files: {os.path.abspath(successfully_read_files[-1])}") + _init_dirs(file_writer_config) config = _parse_config(config_parser, args) camera_config = config - -_init_dirs(file_writer_config) diff --git a/manim/default.cfg b/manim/default.cfg index 1bf723025f..9cf64ede13 100644 --- a/manim/default.cfg +++ b/manim/default.cfg @@ -140,3 +140,16 @@ streaming_console_banner = Manim is now running in streaming mode. Stream animations by passing them to manim.play(), e.g. >>> c = Circle() >>> manim.play(ShowCreation(c)) + +[logger] +logging_keyword = bold yellow +logging_level_notset = dim +logging_level_debug = green +logging_level_info = blue +logging_level_warning = red +logging_level_error = red bold +logging_level_critical = red bold reverse +log_level = +log_time = cyan dim +log_message = +log_path = dim diff --git a/manim/logger.py b/manim/logger.py index 416a06a801..4ac8b8d8a3 100644 --- a/manim/logger.py +++ b/manim/logger.py @@ -1,12 +1,74 @@ +""" +logger.py +--------- +This is the logging library for manim. +This library uses rich for coloured log outputs. + +""" +import configparser import logging + +from rich.console import Console from rich.logging import RichHandler +from rich.theme import Theme +from rich import print as printf +from rich import errors, color + +from .utils.config_utils import _run_config + + +def parse_theme(fp): + config_parser.read(fp) + theme = dict(config_parser["logger"]) + # replaces `_` by `.` as rich understands it + for key in theme: + temp = theme[key] + del theme[key] + key = key.replace("_", ".") + theme[key] = temp + try: + customTheme = Theme(theme) + except (color.ColorParseError, errors.StyleSyntaxError): + customTheme = None + printf( + "[logging.level.error]It seems your colour configuration couldn't be parsed. Loading the default color configuration...[/logging.level.error]" + ) + return customTheme + +config_items = _run_config() +config_parser, successfully_read_files = config_items[1], config_items[-1] +try: + customTheme = parse_theme(successfully_read_files) + console = Console(theme=customTheme) +except KeyError: + console = Console() + printf( + "[logging.level.warning]No cfg file found, creating one in " + + successfully_read_files[0] + + " [/logging.level.warning]" + ) +# These keywords Are Highlighted specially. +RichHandler.KEYWORDS = [ + "Played", + "animations", + "scene", + "Reading", + "Writing", + "script", + "arguments", + "Invalid", + "Aborting", + "module", + "File", + "Rendering", +] logging.basicConfig( level="NOTSET", format="%(message)s", datefmt="[%X]", - handlers=[RichHandler()] + handlers=[RichHandler(console=console)], ) logger = logging.getLogger("rich") diff --git a/manim/utils/cfgwriter.py b/manim/utils/cfgwriter.py new file mode 100644 index 0000000000..3b74dbeb21 --- /dev/null +++ b/manim/utils/cfgwriter.py @@ -0,0 +1,116 @@ +""" +cfgwriter.py +------------ + +Inputs the configuration files while checking it is valid. Can be executed by `manim-cfg` command. + +""" +import os +import configparser + +from .config_utils import _run_config, _paths_config_file + +from rich.console import Console +from rich.progress import track +from rich.style import Style +from rich.errors import StyleSyntaxError + +__all__ = ["main"] + +INVALID_STYLE_MSG = "[red bold]Your Style is not valid. Try again.[/red bold]" +INTRO_INSTRUCTIONS = """[red]The default colour is used by the input statement. +If left empty, the default colour will be used.[/red] +[magenta] For a full list of styles, visit[/magenta] https://rich.readthedocs.io/en/latest/style.html""" +TITLE_TEXT = "[yellow bold]Manim Configuration File Writer[/yellow bold]" + + +def is_valid_style(style): + """Checks whether the entered color is a valid color according to rich + Parameters + ---------- + style : :class:`str` + The style to check whether it is valid. + Returns + ------- + Boolean + Returns whether it is valid style or not according to rich. + """ + try: + Style.parse(style) + return True + except StyleSyntaxError: + return False + + +def replace_keys(default): + """Replaces _ to . and viceversa in a dictionary for rich + Parameters + ---------- + default : :class:`dict` + The dictionary to check and replace + Returns + ------- + :class:`dict` + The dictionary which is modified by replcaing _ with . and viceversa + """ + for key in default: + if "_" in key: + temp = default[key] + del default[key] + key = key.replace("_", ".") + default[key] = temp + else: + temp = default[key] + del default[key] + key = key.replace(".", "_") + default[key] = temp + return default + + +def main(): + config = _run_config()[1] + console = Console() + default = config["logger"] + console.print(TITLE_TEXT, justify="center") + console.print(INTRO_INSTRUCTIONS) + default = replace_keys(default) + for key in default: + console.print("Enter the Style for %s" % key + ":", style=key, end="") + temp = input() + if temp: + while not is_valid_style(temp): + console.print(INVALID_STYLE_MSG) + console.print("Enter the Style for %s" % key + ":", style=key, end="") + temp = input() + else: + default[key] = temp + default = replace_keys(default) + config["logger"] = default + console.print( + "Do you want to save this as the default for this User?(y/n)[[n]]", + style="dim purple", + end="", + ) + save_to_userpath = input() + config_paths = _paths_config_file() + [os.path.abspath("manim.cfg")] + if save_to_userpath.lower() == "y": + if not os.path.exists(os.path.abspath(os.path.join(config_paths[1], ".."))): + os.makedirs(os.path.abspath(os.path.join(config_paths[1], ".."))) + with open(config_paths[1], "w") as fp: + config.write(fp) + console.print( + f"""A configuration file called [yellow]{config_paths[1]}[/yellow] has been created with your required changes. +This will be used when running the manim command. If you want to override this config, +you will have to create a manim.cfg in the local directory, where you want those changes to be overridden.""" + ) + else: + with open(config_paths[2], "w") as fp: + config.write(fp) + console.print( + f"""A configuration file called [yellow]{config_paths[2]}[/yellow] has been created. +To save your theme please save that file and place it in your current working directory, from where you run the manim command.""" + ) + + +if __name__ == "__main__": + main() diff --git a/manim/utils/config_utils.py b/manim/utils/config_utils.py new file mode 100644 index 0000000000..a6c7fb1875 --- /dev/null +++ b/manim/utils/config_utils.py @@ -0,0 +1,399 @@ +""" +config_utils.py +--------------- + +Utility functions for parsing manim config files. + +""" + +import argparse +import configparser +import os +import sys + +import colour + +from .. import constants +from .tex import TexTemplate, TexTemplateFromFile + +__all__ = ["_run_config", "_paths_config_file", "_from_command_line"] + + +def _parse_file_writer_config(config_parser, args): + """Parse config files and CLI arguments into a single dictionary.""" + # By default, use the CLI section of the digested .cfg files + default = config_parser["CLI"] + + # This will be the final file_writer_config dict exposed to the user + fw_config = {} + + # Handle input files and scenes. Note these cannot be set from + # the .cfg files, only from CLI arguments + fw_config["input_file"] = args.file + fw_config["scene_names"] = args.scene_names if args.scene_names is not None else [] + fw_config["output_file"] = args.output_file + + # Handle all options that are directly overridden by CLI + # arguments. Note ConfigParser options are all strings and each + # needs to be converted to the appropriate type. Thus, we do this + # in batches, depending on their type: booleans and strings + for boolean_opt in [ + "preview", + "show_file_in_finder", + "quiet", + "sound", + "leave_progress_bars", + "write_to_movie", + "save_last_frame", + "save_pngs", + "save_as_gif", + "write_all", + ]: + attr = getattr(args, boolean_opt) + fw_config[boolean_opt] = ( + default.getboolean(boolean_opt) if attr is None else attr + ) + # for str_opt in ['media_dir', 'video_dir', 'tex_dir', 'text_dir']: + for str_opt in ["media_dir"]: + attr = getattr(args, str_opt) + fw_config[str_opt] = default[str_opt] if attr is None else attr + dir_names = {"video_dir": "videos", "tex_dir": "Tex", "text_dir": "texts"} + for name in dir_names: + fw_config[name] = os.path.join(fw_config["media_dir"], dir_names[name]) + + # Handle the -s (--save_last_frame) flag: invalidate the -w flag + # At this point the save_last_frame option has already been set by + # both CLI and the cfg file, so read the config dict directly + if fw_config["save_last_frame"]: + fw_config["write_to_movie"] = False + + # Handle the -t (--transparent) flag. This flag determines which + # section to use from the .cfg file. + section = config_parser["transparent"] if args.transparent else default + for opt in ["png_mode", "movie_file_extension", "background_opacity"]: + fw_config[opt] = section[opt] + + # Handle the -n flag. Read first from the cfg and then override with CLI. + # These two are integers -- use getint() + for opt in ["from_animation_number", "upto_animation_number"]: + fw_config[opt] = default.getint(opt) + if fw_config["upto_animation_number"] == -1: + fw_config["upto_animation_number"] = float("inf") + nflag = args.from_animation_number + if nflag is not None: + if "," in nflag: + start, end = nflag.split(",") + fw_config["from_animation_number"] = int(start) + fw_config["upto_animation_number"] = int(end) + else: + fw_config["from_animation_number"] = int(nflag) + + # Handle the --dry_run flag. This flag determines which section + # to use from the .cfg file. All options involved are boolean. + # Note this overrides the flags -w, -s, -a, -g, and -i. + if args.dry_run: + for opt in [ + "write_to_movie", + "save_last_frame", + "save_pngs", + "save_as_gif", + "write_all", + ]: + fw_config[opt] = config_parser["dry_run"].getboolean(opt) + + # Read in the streaming section -- all values are strings + fw_config["streaming"] = { + opt: config_parser["streaming"][opt] + for opt in [ + "live_stream_name", + "twitch_stream_key", + "streaming_protocol", + "streaming_ip", + "streaming_protocol", + "streaming_client", + "streaming_port", + "streaming_port", + "streaming_console_banner", + ] + } + + # For internal use (no CLI flag) + fw_config["skip_animations"] = any( + [fw_config["save_last_frame"], fw_config["from_animation_number"]] + ) + + return fw_config + + +def _parse_cli(arg_list, input=True): + parser = argparse.ArgumentParser( + description="Animation engine for explanatory math videos", + epilog="Made with <3 by the manim community devs", + ) + if input: + parser.add_argument( + "file", help="path to file holding the python code for the scene", + ) + parser.add_argument( + "scene_names", + nargs="*", + help="Name of the Scene class you want to see", + default=[""], + ) + parser.add_argument( + "-o", + "--output_file", + help="Specify the name of the output file, if " + "it should be different from the scene class name", + default="", + ) + + # The following use (action='store_const', const=True) instead of + # the built-in (action='store_true'). This is because the latter + # will default to False if not specified, while the former sets no + # default value. Since we want to set the default value in + # manim.cfg rather than here, we use the former. + parser.add_argument( + "-p", + "--preview", + action="store_const", + const=True, + help="Automatically open the saved file once its done", + ) + parser.add_argument( + "-f", + "--show_file_in_finder", + action="store_const", + const=True, + help="Show the output file in finder", + ) + parser.add_argument( + "-q", "--quiet", action="store_const", const=True, help="Quiet mode", + ) + parser.add_argument( + "--sound", + action="store_const", + const=True, + help="Play a success/failure sound", + ) + parser.add_argument( + "--leave_progress_bars", + action="store_const", + const=True, + help="Leave progress bars displayed in terminal", + ) + parser.add_argument( + "-a", + "--write_all", + action="store_const", + const=True, + help="Write all the scenes from a file", + ) + parser.add_argument( + "-w", + "--write_to_movie", + action="store_const", + const=True, + help="Render the scene as a movie file", + ) + parser.add_argument( + "-s", + "--save_last_frame", + action="store_const", + const=True, + help="Save the last frame (and do not save movie)", + ) + parser.add_argument( + "-g", + "--save_pngs", + action="store_const", + const=True, + help="Save each frame as a png", + ) + parser.add_argument( + "-i", + "--save_as_gif", + action="store_const", + const=True, + help="Save the video as gif", + ) + + # The default value of the following is set in manim.cfg + parser.add_argument( + "-c", "--color", help="Background color", + ) + parser.add_argument( + "--background_opacity", help="Background opacity", + ) + parser.add_argument( + "--media_dir", help="directory to write media", + ) + # video_group = parser.add_mutually_exclusive_group() + # video_group.add_argument( + # "--video_dir", + # help="directory to write file tree for video", + # ) + # parser.add_argument( + # "--tex_dir", + # help="directory to write tex", + # ) + # parser.add_argument( + # "--text_dir", + # help="directory to write text", + # ) + parser.add_argument( + "--tex_template", help="Specify a custom TeX template file", + ) + + # All of the following use (action="store_true"). This means that + # they are by default False. In contrast to the previous ones that + # used (action="store_const", const=True), the following do not + # correspond to a single configuration option. Rather, they + # override several options at the same time. + + # The following overrides -w, -a, -g, and -i + parser.add_argument( + "--dry_run", + action="store_true", + help="Do a dry run (render scenes but generate no output files)", + ) + + # The following overrides PNG_MODE, MOVIE_FILE_EXTENSION, and + # BACKGROUND_OPACITY + parser.add_argument( + "-t", + "--transparent", + action="store_true", + help="Render to a movie file with an alpha channel", + ) + + # The following are mutually exclusive and each overrides + # FRAME_RATE, PIXEL_HEIGHT, and PIXEL_WIDTH, + parser.add_argument( + "-l", + "--low_quality", + action="store_true", + help="Render at low quality (for fastest rendering)", + ) + parser.add_argument( + "-m", + "--medium_quality", + action="store_true", + help="Render at medium quality (for much faster rendering)", + ) + parser.add_argument( + "-e", + "--high_quality", + action="store_true", + help="Render at high quality (for slightly faster rendering)", + ) + parser.add_argument( + "-k", + "--fourk_quality", + action="store_true", + help="Render at 4K quality (slower rendering)", + ) + + # This overrides any of the above + parser.add_argument( + "-r", "--resolution", help='Resolution, passed as "height,width"', + ) + + # This sets FROM_ANIMATION_NUMBER and UPTO_ANIMATION_NUMBER + parser.add_argument( + "-n", + "--from_animation_number", + help="Start rendering not from the first animation, but" + "from another, specified by its index. If you pass" + 'in two comma separated values, e.g. "3,6", it will end' + "the rendering at the second value", + ) + + # Specify the manim.cfg file + parser.add_argument( + "--config_file", help="Specify the configuration file", + ) + + return parser.parse_args(arg_list) + + +def _init_dirs(config): + # Make sure all folders exist + for folder in [ + config["media_dir"], + config["video_dir"], + config["tex_dir"], + config["text_dir"], + ]: + if not os.path.exists(folder): + os.makedirs(folder) + + +def _from_command_line(): + """Determine if manim was called from the command line.""" + # Manim can be called from the command line in three different + # ways. The first two involve using the manim or manimcm commands + prog = os.path.split(sys.argv[0])[-1] + from_cli_command = prog in ["manim", "manimcm"] + + # The third way involves using `python -m manim ...`. In this + # case, the CLI arguments passed to manim do not include 'manim', + # 'manimcm', or even 'python'. However, the -m flag will always + # be the first argument. + from_python_m = sys.argv[0] == "-m" + + return from_cli_command or from_python_m + + +def _paths_config_file(): + library_wide = os.path.abspath( + os.path.join(os.path.dirname(__file__), "..", "default.cfg") + ) + if sys.platform.startswith("linux"): + user_wide = os.path.expanduser( + os.path.join("~", ".config", "manim", "manim.cfg") + ) + elif sys.platform.startswith("darwin"): + user_wide = os.path.expanduser( + os.path.join("~", "Library", "Application Support", "Manim", "manim.cfg") + ) + elif sys.platform.startswith("win32"): + user_wide = os.path.expanduser( + os.path.join("~", "AppData", "Roaming", "Manim", "manim.cfg") + ) + else: + user_wide = os.path.expanduser( + os.path.join("~", ".config", "manim", "manim.cfg") + ) + return [library_wide, user_wide] + + +def _run_config(): + # Config files to be parsed, in ascending priority + config_files = _paths_config_file() + if _from_command_line(): + args = _parse_cli(sys.argv[1:]) + if args.config_file is not None: + if os.path.exists(args.config_file): + config_files.append(args.config_file) + else: + raise FileNotFoundError(f"Config file {args.config_file} doesn't exist") + else: + script_directory_file_config = os.path.join( + os.path.dirname(args.file), "manim.cfg" + ) + if os.path.exists(script_directory_file_config): + config_files.append(script_directory_file_config) + + else: + # In this case, we still need an empty args object. + args = _parse_cli([], input=False) + # Need to populate the options left out + args.file, args.scene_names, args.output_file = "", "", "" + + config_parser = configparser.ConfigParser() + successfully_read_files = config_parser.read(config_files) + + # this is for internal use when writing output files + file_writer_config = _parse_file_writer_config(config_parser, args) + return args, config_parser, file_writer_config, successfully_read_files diff --git a/setup.py b/setup.py index af84f44ad7..2d081bb07e 100755 --- a/setup.py +++ b/setup.py @@ -11,6 +11,7 @@ "console_scripts": [ "manim=manim.__main__:main", "manimcm=manim.__main__:main", + "manim-cfg=manim.utils.cfgwriter:main", ] }, install_requires=[