Skip to content

Terminal

Module: terminaltexteffects.engine.terminal

A class for managing the terminal state and output.

Attributes:

Name Type Description
config TerminalConfig

Configuration for the terminal.

canvas Canvas

The canvas in the terminal.

character_by_input_coord dict[Coord, EffectCharacter]

A dictionary of characters by their input coordinates.

Methods:

Name Description
get_piped_input

Gets the piped input from stdin.

prep_canvas

Prepares the terminal for the effect by adding empty lines and hiding the cursor.

restore_cursor

Restores the cursor visibility.

get_characters

Get a list of all EffectCharacters in the terminal with an optional sort.

get_characters_grouped

Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping.

get_character_by_input_coord

Get an EffectCharacter by its input coordinates.

set_character_visibility

Set the visibility of a character.

get_formatted_output_string

Get the formatted output string based on the current terminal state.

print

Prints the current terminal state to stdout while preserving the cursor position.

Source code in terminaltexteffects/engine/terminal.py
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
class Terminal:
    """A class for managing the terminal state and output.

    Attributes:
        config (TerminalConfig): Configuration for the terminal.
        canvas (Canvas): The canvas in the terminal.
        character_by_input_coord (dict[Coord, EffectCharacter]): A dictionary of characters by their input coordinates.

    Methods:
        get_piped_input:
            Gets the piped input from stdin.
        prep_canvas:
            Prepares the terminal for the effect by adding empty lines and hiding the cursor.
        restore_cursor:
            Restores the cursor visibility.
        get_characters:
            Get a list of all EffectCharacters in the terminal with an optional sort.
        get_characters_grouped:
            Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping.
        get_character_by_input_coord:
            Get an EffectCharacter by its input coordinates.
        set_character_visibility:
            Set the visibility of a character.
        get_formatted_output_string:
            Get the formatted output string based on the current terminal state.
        print:
            Prints the current terminal state to stdout while preserving the cursor position.

    """

    ansi_sequence_color_map: typing.ClassVar[dict[str, Color]] = {}

    class CharacterGroup(Enum):
        """An enum specifying character groupings.

        Attributes:
            COLUMN_LEFT_TO_RIGHT: Group characters by column from left to right.
            COLUMN_RIGHT_TO_LEFT: Group characters by column from right to left.
            ROW_TOP_TO_BOTTOM: Group characters by row from top to bottom.
            ROW_BOTTOM_TO_TOP: Group characters by row from bottom to top.
            DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT: Group characters by diagonal from top left to bottom right.
            DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT: Group characters by diagonal from bottom left to top right.
            DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT: Group characters by diagonal from top right to bottom left.
            DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT: Group characters by diagonal from bottom right to top left.
            CENTER_TO_OUTSIDE_DIAMONDS: Group characters by distance from the center to the outside in diamond shapes.
            OUTSIDE_TO_CENTER_DIAMONDS: Group characters by distance from the outside to the center in diamond shapes.

        """

        COLUMN_LEFT_TO_RIGHT = auto()
        COLUMN_RIGHT_TO_LEFT = auto()
        ROW_TOP_TO_BOTTOM = auto()
        ROW_BOTTOM_TO_TOP = auto()
        DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT = auto()
        DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT = auto()
        DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT = auto()
        DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT = auto()
        CENTER_TO_OUTSIDE_DIAMONDS = auto()
        OUTSIDE_TO_CENTER_DIAMONDS = auto()

    class CharacterSort(Enum):
        """An enum for specifying character sorts.

        Attributes:
            RANDOM: Random order.
            TOP_TO_BOTTOM_LEFT_TO_RIGHT: Top to bottom, left to right.
            TOP_TO_BOTTOM_RIGHT_TO_LEFT: Top to bottom, right to left.
            BOTTOM_TO_TOP_LEFT_TO_RIGHT: Bottom to top, left to right.
            BOTTOM_TO_TOP_RIGHT_TO_LEFT: Bottom to top, right to left.
            OUTSIDE_ROW_TO_MIDDLE: Outside row to middle.
            MIDDLE_ROW_TO_OUTSIDE: Middle row to outside.

        """

        RANDOM = auto()
        TOP_TO_BOTTOM_LEFT_TO_RIGHT = auto()
        TOP_TO_BOTTOM_RIGHT_TO_LEFT = auto()
        BOTTOM_TO_TOP_LEFT_TO_RIGHT = auto()
        BOTTOM_TO_TOP_RIGHT_TO_LEFT = auto()
        OUTSIDE_ROW_TO_MIDDLE = auto()
        MIDDLE_ROW_TO_OUTSIDE = auto()

    class ColorSort(Enum):
        """An enum for specifying color sorts for the colors derived from the input text ansi sequences.

        Attributes:
            LEAST_TO_MOST: Sort colors from least to most frequent.
            MOST_TO_LEAST: Sort colors from most to least frequent.
            RANDOM: Random order.

        """

        LEAST_TO_MOST = auto()
        MOST_TO_LEAST = auto()
        RANDOM = auto()

    def __init__(self, input_data: str, config: TerminalConfig | None = None) -> None:
        """Initialize the Terminal.

        Args:
            input_data (str): The input data to be displayed in the terminal.
            config (TerminalConfig, optional): Configuration for the terminal. Defaults to None.

        """
        if config is None:
            self.config = TerminalConfig()
        else:
            self.config = config
        if not input_data:
            input_data = "No Input."
        self._next_character_id = 0
        self._input_colors_frequency: dict[Color, int] = {}
        self._preprocessed_character_lines = self._preprocess_input_data(input_data)
        self._terminal_width, self._terminal_height = self._get_terminal_dimensions()
        self.canvas = Canvas(*self._get_canvas_dimensions())
        if not self.config.ignore_terminal_dimensions:
            self.canvas_column_offset, self.canvas_row_offset = self._calc_canvas_offsets()
        else:
            self.canvas_column_offset = self.canvas_row_offset = 0
            self._terminal_width = self.canvas.right
            self._terminal_height = self.canvas.top
        # the visible_* attributes are used to determine which characters are visible on the terminal
        self.visible_top = min(self.canvas.top + self.canvas_row_offset, self._terminal_height)
        self.visible_bottom = max(self.canvas.bottom + self.canvas_row_offset, 1)
        self.visible_right = min(self.canvas.right + self.canvas_column_offset, self._terminal_width)
        self.visible_left = max(self.canvas.left + self.canvas_column_offset, 1)
        self._input_characters = [
            character
            for character in self._setup_input_characters()
            if character.input_coord.row <= self.canvas.top and character.input_coord.column <= self.canvas.right
        ]
        self._added_characters: list[EffectCharacter] = []
        self.character_by_input_coord: dict[Coord, EffectCharacter] = {
            (character.input_coord): character for character in self._input_characters
        }
        self._inner_fill_characters, self._outer_fill_characters = self._make_fill_characters()
        self._visible_characters: set[EffectCharacter] = set()
        self._frame_rate = self.config.frame_rate
        self._last_time_printed = time.time()
        self._update_terminal_state()

    def _preprocess_input_data(self, input_data: str) -> list[list[EffectCharacter]]:  # noqa: PLR0915
        """Preprocess the input data.

        Preprocess the input data by replacing tabs with spaces and decomposing the input data into a list of
        characters while applying any active SGR foreground/background ANSI escape sequences discovered in the data.

        Args:
            input_data (str): The input data to be displayed in the terminal.

        Returns:
            list[EffectCharacter]: A list of characters decomposed from the input data.

        """

        def find_ansi_sequences_with_positions(text: str) -> list[tuple[int, int]]:  # [(start,end), ...]
            """Find SGR foreground and background ANSI escape sequences in the input text and return their positions.

            Args:
                text (str): The input text.

            Returns:
                list[tuple[int, int]]: A list of tuples containing the start and end positions of the
                    ANSI escape sequences.

            """
            # match all SGR sequences, though only 8bit and 24bit color sequences will be used, the others are ignored
            ansi_escape_pattern = r"(\x1b|\x1B|\033)\[[\d;]*m"
            matches = re.finditer(ansi_escape_pattern, text)
            return [(match.start(), match.end() - 1) for match in matches]

        characters: list[list[EffectCharacter]] = []
        # replace tabs with spaces
        input_data = input_data.replace("\t", " " * self.config.tab_width)
        # remove trailing whitespace from each line
        input_data_lines = input_data.splitlines()
        input_data = ""
        for line in input_data_lines:
            input_data += line.rstrip() + "\n"
        # find ansi sequences
        sequence_list = find_ansi_sequences_with_positions(input_data)
        active_sequences = {"fg_color": "", "bg_color": ""}
        char_index = 0
        current_character_line: list[EffectCharacter] = []
        while char_index < len(input_data):
            if input_data[char_index] == "\n":
                characters.append(current_character_line)
                current_character_line = []
                char_index += 1
            elif sequence_list and char_index == sequence_list[0][0]:
                active_sequence = input_data[sequence_list[0][0] : sequence_list[0][1] + 1]
                # only apply sequences that are 8bit or 24bit color (only support RGB colorspace, though 38;3 and 38;4
                # exist and indicate CMY and CMYK colorspaces, respectively)
                # match foreground colors
                if re.match(r"(\x1b|\x1B|\033)\[38;(2|5)", active_sequence):
                    active_sequences["fg_color"] = active_sequence
                # match background colors
                elif re.match(r"(\x1b|\x1B|\033)\[48;(2|5)", active_sequence):
                    active_sequences["bg_color"] = active_sequence
                # match reset sequence and clear active sequences
                elif re.match(r"(\x1b|\x1B|\033)\[0?m", active_sequence):
                    active_sequences["fg_color"] = active_sequences["bg_color"] = ""
                char_index = sequence_list[0][1] + 1
                sequence_list.pop(0)
            else:
                character = EffectCharacter(self._next_character_id, input_data[char_index], 0, 0)
                for sequence_type, sequence in active_sequences.items():
                    if sequence:
                        character._input_ansi_sequences[sequence_type] = sequence
                        if sequence in Terminal.ansi_sequence_color_map:
                            color = Terminal.ansi_sequence_color_map[sequence]
                        else:
                            color = Color(ansitools.parse_ansi_color_sequence(sequence))

                            Terminal.ansi_sequence_color_map[sequence] = color
                        if color in self._input_colors_frequency:
                            self._input_colors_frequency[color] += 1
                        else:
                            self._input_colors_frequency[color] = 1
                        if sequence_type == "fg_color":
                            character.animation.input_fg_color = color
                        else:
                            character.animation.input_bg_color = color
                character.animation.no_color = self.config.no_color
                character.animation.use_xterm_colors = self.config.xterm_colors
                character.animation.existing_color_handling = self.config.existing_color_handling
                # if existing_color_handling is set to 'always', set the appearance to the input symbol with
                # any existing color sequences
                if character.animation.existing_color_handling == "always":
                    character.animation.set_appearance(character.input_symbol)
                current_character_line.append(character)
                self._next_character_id += 1
                char_index += 1

        return characters

    def _calc_canvas_offsets(self) -> tuple[int, int]:
        """Calculate the canvas offsets based on the anchor point.

        Returns:
            tuple[int, int]: Canvas column offset, row offset

        """
        canvas_column_offset = canvas_row_offset = 0
        if self.config.anchor_canvas in ("s", "n", "c"):
            canvas_column_offset = (self._terminal_width // 2) - (self.canvas.width // 2)
        elif self.config.anchor_canvas in ("se", "e", "ne"):
            canvas_column_offset = self._terminal_width - self.canvas.width
        if self.config.anchor_canvas in ("w", "e", "c"):
            canvas_row_offset = self._terminal_height // 2 - self.canvas.height // 2
        elif self.config.anchor_canvas in ("nw", "n", "ne"):
            canvas_row_offset = self._terminal_height - self.canvas.height
        return canvas_column_offset, canvas_row_offset

    def _get_canvas_dimensions(self) -> tuple[int, int]:
        """Determine the canvas dimensions using the input data dimensions, terminal dimensions, and text wrapping.

        Returns:
            tuple[int, int]: Canvas height, width.

        """
        if self.config.canvas_width > 0:
            canvas_width = self.config.canvas_width
        elif self.config.canvas_width == 0:
            canvas_width = self._terminal_width
        else:
            input_width = max([len(line) for line in self._preprocessed_character_lines])
            if self.config.ignore_terminal_dimensions:
                canvas_width = input_width
            else:
                canvas_width = min(self._terminal_width, input_width)
        if self.config.canvas_height > 0:
            canvas_height = self.config.canvas_height
        elif self.config.canvas_height == 0:
            canvas_height = self._terminal_height
        else:
            input_height = len(self._preprocessed_character_lines)
            if self.config.ignore_terminal_dimensions:
                canvas_height = input_height
            elif self.config.wrap_text:
                canvas_height = min(
                    len(self._wrap_lines(self._preprocessed_character_lines, canvas_width)),
                    self._terminal_height,
                )
            else:
                canvas_height = min(self._terminal_height, input_height)

        return canvas_height, canvas_width

    def _get_terminal_dimensions(self) -> tuple[int, int]:
        """Get the terminal dimensions.

        Use shutil.get_terminal_size() to get the terminal dimensions. If the terminal size cannot be determined,
        default values of 80 columns and 24 rows are returned.

        Returns:
            tuple[int, int]: terminal width and height

        """
        try:
            terminal_width, terminal_height = shutil.get_terminal_size()
        except OSError:
            # If the terminal size cannot be determined, return default values
            return 80, 24
        return terminal_width, terminal_height

    @staticmethod
    def get_piped_input() -> str:
        """Get the piped input from stdin.

        This method checks if there is any piped input from the standard input (stdin).
        If there is no piped input, it returns an empty string.
        If there is piped input, it reads the input data from stdin and returns it as a string.

        The `sys.stdin.isatty()` check is used to determine if the program is being run interactively
        or if there is piped input. When the program is run interactively, `sys.stdin.isatty()` returns True,
        indicating that there is no piped input. In this case, the method returns an empty string.

        Returns:
            str: The piped input from stdin as a string, or an empty string if there is no piped input.

        """
        if sys.stdin.isatty():
            return ""
        return sys.stdin.read()

    def _wrap_lines(self, lines: list[list[EffectCharacter]], width: int) -> list[list[EffectCharacter]]:
        """Wrap the given lines of text to fit within the width of the canvas.

        Args:
            lines (list[list[EffectCharacter]]): The lines of text to be wrapped.
            width (int): The maximum length of a line.

        Returns:
            list: The wrapped lines of text.

        """
        wrapped_lines = []
        for line in lines:
            current_line = line
            while len(current_line) > width:
                wrapped_lines.append(current_line[:width])
                current_line = current_line[width:]
            wrapped_lines.append(current_line)
        return wrapped_lines

    def _setup_input_characters(self) -> list[EffectCharacter]:
        """Set up the input characters discovered during preprocessing.

        Characters are positioned based on row/column coordinates relative to the anchor point in the Canvas.

        Coordinates are relative to the cursor row position at the time of execution. 1,1 is the bottom left
        corner of the row above the cursor.

        Returns:
            list[Character]: list of EffectCharacter objects

        """
        formatted_lines = []
        formatted_lines = (
            self._wrap_lines(self._preprocessed_character_lines, self.canvas.right)
            if self.config.wrap_text
            else self._preprocessed_character_lines
        )
        input_height = len(formatted_lines)
        input_characters: list[EffectCharacter] = []
        for row, line in enumerate(formatted_lines):
            for column, character in enumerate(line, start=1):
                character._input_coord = Coord(column, input_height - row)
                if character._input_symbol != " ":
                    input_characters.append(character)

        anchored_characters = self.canvas._anchor_text(input_characters, self.config.anchor_text)
        return [char for char in anchored_characters if self.canvas.coord_is_in_canvas(char._input_coord)]

    def _make_fill_characters(self) -> tuple[list[EffectCharacter], list[EffectCharacter]]:
        """Create lists of characters to fill the empty spaces in the canvas.

        The characters input_symbol is a space. The characters are added to the character_by_input_coord dictionary.

        Returns:
            tuple[list[EffectCharacter], list[EffectCharacter]]: lists of inner and outer fill characters

        """
        inner_fill_characters = []
        outer_fill_characters = []
        for row in range(1, self.canvas.top + 1):
            for column in range(1, self.canvas.right + 1):
                coord = Coord(column, row)
                if coord not in self.character_by_input_coord:
                    fill_char = EffectCharacter(self._next_character_id, " ", column, row)
                    fill_char.is_fill_character = True
                    fill_char.animation.no_color = self.config.no_color
                    fill_char.animation.use_xterm_colors = self.config.xterm_colors
                    fill_char.animation.existing_color_handling = self.config.existing_color_handling
                    self.character_by_input_coord[coord] = fill_char
                    self._next_character_id += 1
                    if (
                        self.canvas.text_left <= column <= self.canvas.text_right
                        and self.canvas.text_bottom <= row <= self.canvas.text_top
                    ):
                        inner_fill_characters.append(fill_char)
                    else:
                        outer_fill_characters.append(fill_char)
        return inner_fill_characters, outer_fill_characters

    def add_character(self, symbol: str, coord: Coord) -> EffectCharacter:
        """Add a character to the terminal for printing.

        Used to create characters that are not in the input data.

        Args:
            symbol (str): symbol to add
            coord: (Coord): set character's input coordinates

        Returns:
            EffectCharacter: the character that was added

        """
        character = EffectCharacter(self._next_character_id, symbol, coord.column, coord.row)
        character.animation.no_color = self.config.no_color
        character.animation.use_xterm_colors = self.config.xterm_colors
        character.animation.existing_color_handling = self.config.existing_color_handling

        self._added_characters.append(character)
        self._next_character_id += 1
        return character

    def get_input_colors(self, sort: ColorSort = ColorSort.MOST_TO_LEAST) -> list[Color]:
        """Get a list of colors derived from the input text ansi sequences with an optional sort.

        Args:
            sort (ColorSort, optional): Sort the colors. Defaults to ColorSort.MOST_TO_LEAST.

        Raises:
            ValueError: If an invalid sort option is provided.

        Returns:
            list[Color]: list of Colors

        """
        if sort == self.ColorSort.MOST_TO_LEAST:
            return sorted(
                self._input_colors_frequency.keys(),
                key=lambda color: self._input_colors_frequency[color],
                reverse=True,
            )
        if sort == self.ColorSort.RANDOM:
            colors = list(self._input_colors_frequency.keys())
            random.shuffle(colors)
            return colors
        if sort == self.ColorSort.LEAST_TO_MOST:
            return sorted(self._input_colors_frequency.keys(), key=lambda color: self._input_colors_frequency[color])
        raise InvalidColorSortError(sort)

    def get_characters(
        self,
        *,
        input_chars: bool = True,
        inner_fill_chars: bool = False,
        outer_fill_chars: bool = False,
        added_chars: bool = False,
        sort: CharacterSort = CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT,
    ) -> list[EffectCharacter]:
        """Get a list of all EffectCharacters in the terminal with an optional sort.

        Args:
            input_chars (bool, optional): whether to include input characters. Defaults to True.
            inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False.
            outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False.
            added_chars (bool, optional): whether to include added characters. Defaults to False.
            sort (CharacterSort, optional): order to sort the characters.
                Defaults to CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT.

        Returns:
            list[EffectCharacter]: list of EffectCharacters in the terminal

        """
        all_characters: list[EffectCharacter] = []
        if input_chars:
            all_characters.extend(self._input_characters)
        if inner_fill_chars:
            all_characters.extend(self._inner_fill_characters)
        if outer_fill_chars:
            all_characters.extend(self._outer_fill_characters)
        if added_chars:
            all_characters.extend(self._added_characters)

        # default sort TOP_TO_BOTTOM_LEFT_TO_RIGHT
        all_characters.sort(key=lambda character: (-character.input_coord.row, character.input_coord.column))

        if sort is self.CharacterSort.RANDOM:
            random.shuffle(all_characters)

        elif sort in (self.CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT, self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT):
            if sort is self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT:
                all_characters.reverse()

        elif sort in (self.CharacterSort.BOTTOM_TO_TOP_LEFT_TO_RIGHT, self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT):
            all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column))
            if sort is self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT:
                all_characters.reverse()

        elif sort in (self.CharacterSort.OUTSIDE_ROW_TO_MIDDLE, self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE):
            all_characters = [
                all_characters.pop(0) if i % 2 == 0 else all_characters.pop(-1) for i in range(len(all_characters))
            ]
            if sort is self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE:
                all_characters.reverse()
        else:
            raise InvalidCharacterSortError(sort)

        return all_characters

    def get_characters_grouped(
        self,
        grouping: CharacterGroup = CharacterGroup.ROW_TOP_TO_BOTTOM,
        *,
        input_chars: bool = True,
        inner_fill_chars: bool = False,
        outer_fill_chars: bool = False,
        added_chars: bool = False,
    ) -> list[list[EffectCharacter]]:
        """Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping.

        Args:
            grouping (CharacterGroup, optional): order to group the characters. Defaults to ROW_TOP_TO_BOTTOM.
            input_chars (bool, optional): whether to include input characters. Defaults to True.
            inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False.
            outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False.
            added_chars (bool, optional): whether to include added characters. Defaults to False.

        Returns:
            list[list[EffectCharacter]]: list of lists of EffectCharacters in the terminal. Inner lists correspond
                to groups as specified in the grouping.

        """
        all_characters: list[EffectCharacter] = []
        if input_chars:
            all_characters.extend(self._input_characters)
        if inner_fill_chars:
            all_characters.extend(self._inner_fill_characters)
        if outer_fill_chars:
            all_characters.extend(self._outer_fill_characters)
        if added_chars:
            all_characters.extend(self._added_characters)

        all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column))

        if grouping in (self.CharacterGroup.COLUMN_LEFT_TO_RIGHT, self.CharacterGroup.COLUMN_RIGHT_TO_LEFT):
            columns = []
            for column_index in range(self.canvas.right + 1):
                characters_in_column = [
                    character for character in all_characters if character.input_coord.column == column_index
                ]
                if characters_in_column:
                    columns.append(characters_in_column)
            if grouping == self.CharacterGroup.COLUMN_RIGHT_TO_LEFT:
                columns.reverse()
            return columns

        if grouping in (self.CharacterGroup.ROW_BOTTOM_TO_TOP, self.CharacterGroup.ROW_TOP_TO_BOTTOM):
            rows = []
            for row_index in range(self.canvas.top + 1):
                characters_in_row = [
                    character for character in all_characters if character.input_coord.row == row_index
                ]
                if characters_in_row:
                    rows.append(characters_in_row)
            if grouping == self.CharacterGroup.ROW_TOP_TO_BOTTOM:
                rows.reverse()
            return rows
        if grouping in (
            self.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT,
            self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT,
        ):
            diagonals = []
            for diagonal_index in range(self.canvas.top + self.canvas.right + 1):
                characters_in_diagonal = [
                    character
                    for character in all_characters
                    if character.input_coord.row + character.input_coord.column == diagonal_index
                ]
                if characters_in_diagonal:
                    diagonals.append(characters_in_diagonal)
            if grouping == self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT:
                diagonals.reverse()
            return diagonals
        if grouping in (
            self.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT,
            self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT,
        ):
            diagonals = []
            for diagonal_index in range(self.canvas.left - self.canvas.top, self.canvas.right - self.canvas.bottom + 1):
                characters_in_diagonal = [
                    character
                    for character in all_characters
                    if character.input_coord.column - character.input_coord.row == diagonal_index
                ]
                if characters_in_diagonal:
                    diagonals.append(characters_in_diagonal)
            if grouping == self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT:
                diagonals.reverse()
            return diagonals
        if grouping in (
            self.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS,
            self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
        ):
            distance_map: dict[int, list[EffectCharacter]] = {}
            center_out = []
            for character in all_characters:
                distance = abs(character.input_coord.column - self.canvas.center.column) + abs(
                    character.input_coord.row - self.canvas.center.row,
                )
                if distance not in distance_map:
                    distance_map[distance] = []
                distance_map[distance].append(character)
            for distance in sorted(
                distance_map.keys(),
                reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
            ):
                center_out = [
                    distance_map[distance]
                    for distance in sorted(
                        distance_map.keys(),
                        reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
                    )
                ]
            return center_out

        raise InvalidCharacterGroupError(grouping)

    def get_character_by_input_coord(self, coord: Coord) -> EffectCharacter | None:
        """Get an EffectCharacter by its input coordinates.

        Args:
            coord (Coord): input coordinates of the character

        Returns:
            EffectCharacter | None: the character at the specified coordinates, or None if no character is found

        """
        return self.character_by_input_coord.get(coord, None)

    def set_character_visibility(self, character: EffectCharacter, is_visible: bool) -> None:  # noqa: FBT001
        """Set the visibility of a character.

        Args:
            character (EffectCharacter): the character to set visibility for
            is_visible (bool): whether the character should be visible

        """
        character._is_visible = is_visible
        if is_visible:
            self._visible_characters.add(character)
        else:
            self._visible_characters.discard(character)

    def get_formatted_output_string(self) -> str:
        """Get the formatted output string based on the current terminal state.

        This method updates the internal terminal representation state before returning the formatted output string.

        Returns:
            str: The formatted output string.

        """
        self._update_terminal_state()
        return "\n".join(self.terminal_state[::-1])

    def _update_terminal_state(self) -> None:
        """Update the internal representation of the terminal state.

        The terminal state is updated with the current position of all visible characters.
        """
        rows = [[" " for _ in range(self.visible_right)] for _ in range(self.visible_top)]
        for character in sorted(self._visible_characters, key=lambda c: c.layer):
            row = character.motion.current_coord.row + self.canvas_row_offset
            column = character.motion.current_coord.column + self.canvas_column_offset
            if self.visible_bottom <= row <= self.visible_top and self.visible_left <= column <= self.visible_right:
                rows[row - 1][column - 1] = character.animation.current_character_visual.formatted_symbol
        terminal_state = ["".join(row) for row in rows]
        self.terminal_state = terminal_state

    def prep_canvas(self) -> None:
        """Prepare the terminal for the effect by adding empty lines and hiding the cursor."""
        sys.stdout.write(ansitools.hide_cursor())
        sys.stdout.write("\n" * (self.visible_top))
        sys.stdout.write(ansitools.dec_save_cursor_position())

    def restore_cursor(self, end_symbol: str = "\n") -> None:
        """Restores the cursor visibility and prints the end_symbol.

        Args:
            end_symbol (str, optional): The symbol to print after the effect has completed. Defaults to newline.

        """
        sys.stdout.write(ansitools.show_cursor())
        sys.stdout.write(end_symbol)

    def print(self, output_string: str) -> None:
        """Print the current terminal state to stdout while preserving the cursor position.

        Args:
            output_string (str): The string to be printed.

        """
        self.move_cursor_to_top()
        sys.stdout.write(output_string)
        sys.stdout.flush()

    def enforce_framerate(self) -> None:
        """Enforce the frame rate set in the terminal config.

        Frame rate is enforced by sleeping if the time since the last frame is shorter than the expected frame delay.
        """
        frame_delay = 1 / self._frame_rate
        time_since_last_print = time.time() - self._last_time_printed
        if time_since_last_print < frame_delay:
            time.sleep(frame_delay - time_since_last_print)
        self._last_time_printed = time.time()

    def move_cursor_to_top(self) -> None:
        """Restores the cursor position to the top of the canvas."""
        sys.stdout.write(ansitools.dec_restore_cursor_position())
        sys.stdout.write(ansitools.dec_save_cursor_position())
        sys.stdout.write(ansitools.move_cursor_up(self.visible_top))

CharacterGroup

Bases: Enum

An enum specifying character groupings.

Attributes:

Name Type Description
COLUMN_LEFT_TO_RIGHT

Group characters by column from left to right.

COLUMN_RIGHT_TO_LEFT

Group characters by column from right to left.

ROW_TOP_TO_BOTTOM

Group characters by row from top to bottom.

ROW_BOTTOM_TO_TOP

Group characters by row from bottom to top.

DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT

Group characters by diagonal from top left to bottom right.

DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT

Group characters by diagonal from bottom left to top right.

DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT

Group characters by diagonal from top right to bottom left.

DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT

Group characters by diagonal from bottom right to top left.

CENTER_TO_OUTSIDE_DIAMONDS

Group characters by distance from the center to the outside in diamond shapes.

OUTSIDE_TO_CENTER_DIAMONDS

Group characters by distance from the outside to the center in diamond shapes.

Source code in terminaltexteffects/engine/terminal.py
class CharacterGroup(Enum):
    """An enum specifying character groupings.

    Attributes:
        COLUMN_LEFT_TO_RIGHT: Group characters by column from left to right.
        COLUMN_RIGHT_TO_LEFT: Group characters by column from right to left.
        ROW_TOP_TO_BOTTOM: Group characters by row from top to bottom.
        ROW_BOTTOM_TO_TOP: Group characters by row from bottom to top.
        DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT: Group characters by diagonal from top left to bottom right.
        DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT: Group characters by diagonal from bottom left to top right.
        DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT: Group characters by diagonal from top right to bottom left.
        DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT: Group characters by diagonal from bottom right to top left.
        CENTER_TO_OUTSIDE_DIAMONDS: Group characters by distance from the center to the outside in diamond shapes.
        OUTSIDE_TO_CENTER_DIAMONDS: Group characters by distance from the outside to the center in diamond shapes.

    """

    COLUMN_LEFT_TO_RIGHT = auto()
    COLUMN_RIGHT_TO_LEFT = auto()
    ROW_TOP_TO_BOTTOM = auto()
    ROW_BOTTOM_TO_TOP = auto()
    DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT = auto()
    DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT = auto()
    DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT = auto()
    DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT = auto()
    CENTER_TO_OUTSIDE_DIAMONDS = auto()
    OUTSIDE_TO_CENTER_DIAMONDS = auto()

CharacterSort

Bases: Enum

An enum for specifying character sorts.

Attributes:

Name Type Description
RANDOM

Random order.

TOP_TO_BOTTOM_LEFT_TO_RIGHT

Top to bottom, left to right.

TOP_TO_BOTTOM_RIGHT_TO_LEFT

Top to bottom, right to left.

BOTTOM_TO_TOP_LEFT_TO_RIGHT

Bottom to top, left to right.

BOTTOM_TO_TOP_RIGHT_TO_LEFT

Bottom to top, right to left.

OUTSIDE_ROW_TO_MIDDLE

Outside row to middle.

MIDDLE_ROW_TO_OUTSIDE

Middle row to outside.

Source code in terminaltexteffects/engine/terminal.py
class CharacterSort(Enum):
    """An enum for specifying character sorts.

    Attributes:
        RANDOM: Random order.
        TOP_TO_BOTTOM_LEFT_TO_RIGHT: Top to bottom, left to right.
        TOP_TO_BOTTOM_RIGHT_TO_LEFT: Top to bottom, right to left.
        BOTTOM_TO_TOP_LEFT_TO_RIGHT: Bottom to top, left to right.
        BOTTOM_TO_TOP_RIGHT_TO_LEFT: Bottom to top, right to left.
        OUTSIDE_ROW_TO_MIDDLE: Outside row to middle.
        MIDDLE_ROW_TO_OUTSIDE: Middle row to outside.

    """

    RANDOM = auto()
    TOP_TO_BOTTOM_LEFT_TO_RIGHT = auto()
    TOP_TO_BOTTOM_RIGHT_TO_LEFT = auto()
    BOTTOM_TO_TOP_LEFT_TO_RIGHT = auto()
    BOTTOM_TO_TOP_RIGHT_TO_LEFT = auto()
    OUTSIDE_ROW_TO_MIDDLE = auto()
    MIDDLE_ROW_TO_OUTSIDE = auto()

ColorSort

Bases: Enum

An enum for specifying color sorts for the colors derived from the input text ansi sequences.

Attributes:

Name Type Description
LEAST_TO_MOST

Sort colors from least to most frequent.

MOST_TO_LEAST

Sort colors from most to least frequent.

RANDOM

Random order.

Source code in terminaltexteffects/engine/terminal.py
class ColorSort(Enum):
    """An enum for specifying color sorts for the colors derived from the input text ansi sequences.

    Attributes:
        LEAST_TO_MOST: Sort colors from least to most frequent.
        MOST_TO_LEAST: Sort colors from most to least frequent.
        RANDOM: Random order.

    """

    LEAST_TO_MOST = auto()
    MOST_TO_LEAST = auto()
    RANDOM = auto()

__init__(input_data, config=None)

Initialize the Terminal.

Parameters:

Name Type Description Default
input_data str

The input data to be displayed in the terminal.

required
config TerminalConfig

Configuration for the terminal. Defaults to None.

None
Source code in terminaltexteffects/engine/terminal.py
def __init__(self, input_data: str, config: TerminalConfig | None = None) -> None:
    """Initialize the Terminal.

    Args:
        input_data (str): The input data to be displayed in the terminal.
        config (TerminalConfig, optional): Configuration for the terminal. Defaults to None.

    """
    if config is None:
        self.config = TerminalConfig()
    else:
        self.config = config
    if not input_data:
        input_data = "No Input."
    self._next_character_id = 0
    self._input_colors_frequency: dict[Color, int] = {}
    self._preprocessed_character_lines = self._preprocess_input_data(input_data)
    self._terminal_width, self._terminal_height = self._get_terminal_dimensions()
    self.canvas = Canvas(*self._get_canvas_dimensions())
    if not self.config.ignore_terminal_dimensions:
        self.canvas_column_offset, self.canvas_row_offset = self._calc_canvas_offsets()
    else:
        self.canvas_column_offset = self.canvas_row_offset = 0
        self._terminal_width = self.canvas.right
        self._terminal_height = self.canvas.top
    # the visible_* attributes are used to determine which characters are visible on the terminal
    self.visible_top = min(self.canvas.top + self.canvas_row_offset, self._terminal_height)
    self.visible_bottom = max(self.canvas.bottom + self.canvas_row_offset, 1)
    self.visible_right = min(self.canvas.right + self.canvas_column_offset, self._terminal_width)
    self.visible_left = max(self.canvas.left + self.canvas_column_offset, 1)
    self._input_characters = [
        character
        for character in self._setup_input_characters()
        if character.input_coord.row <= self.canvas.top and character.input_coord.column <= self.canvas.right
    ]
    self._added_characters: list[EffectCharacter] = []
    self.character_by_input_coord: dict[Coord, EffectCharacter] = {
        (character.input_coord): character for character in self._input_characters
    }
    self._inner_fill_characters, self._outer_fill_characters = self._make_fill_characters()
    self._visible_characters: set[EffectCharacter] = set()
    self._frame_rate = self.config.frame_rate
    self._last_time_printed = time.time()
    self._update_terminal_state()

add_character(symbol, coord)

Add a character to the terminal for printing.

Used to create characters that are not in the input data.

Parameters:

Name Type Description Default
symbol str

symbol to add

required
coord Coord

(Coord): set character's input coordinates

required

Returns:

Name Type Description
EffectCharacter EffectCharacter

the character that was added

Source code in terminaltexteffects/engine/terminal.py
def add_character(self, symbol: str, coord: Coord) -> EffectCharacter:
    """Add a character to the terminal for printing.

    Used to create characters that are not in the input data.

    Args:
        symbol (str): symbol to add
        coord: (Coord): set character's input coordinates

    Returns:
        EffectCharacter: the character that was added

    """
    character = EffectCharacter(self._next_character_id, symbol, coord.column, coord.row)
    character.animation.no_color = self.config.no_color
    character.animation.use_xterm_colors = self.config.xterm_colors
    character.animation.existing_color_handling = self.config.existing_color_handling

    self._added_characters.append(character)
    self._next_character_id += 1
    return character

enforce_framerate()

Enforce the frame rate set in the terminal config.

Frame rate is enforced by sleeping if the time since the last frame is shorter than the expected frame delay.

Source code in terminaltexteffects/engine/terminal.py
def enforce_framerate(self) -> None:
    """Enforce the frame rate set in the terminal config.

    Frame rate is enforced by sleeping if the time since the last frame is shorter than the expected frame delay.
    """
    frame_delay = 1 / self._frame_rate
    time_since_last_print = time.time() - self._last_time_printed
    if time_since_last_print < frame_delay:
        time.sleep(frame_delay - time_since_last_print)
    self._last_time_printed = time.time()

get_character_by_input_coord(coord)

Get an EffectCharacter by its input coordinates.

Parameters:

Name Type Description Default
coord Coord

input coordinates of the character

required

Returns:

Type Description
EffectCharacter | None

EffectCharacter | None: the character at the specified coordinates, or None if no character is found

Source code in terminaltexteffects/engine/terminal.py
def get_character_by_input_coord(self, coord: Coord) -> EffectCharacter | None:
    """Get an EffectCharacter by its input coordinates.

    Args:
        coord (Coord): input coordinates of the character

    Returns:
        EffectCharacter | None: the character at the specified coordinates, or None if no character is found

    """
    return self.character_by_input_coord.get(coord, None)

get_characters(*, input_chars=True, inner_fill_chars=False, outer_fill_chars=False, added_chars=False, sort=CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT)

Get a list of all EffectCharacters in the terminal with an optional sort.

Parameters:

Name Type Description Default
input_chars bool

whether to include input characters. Defaults to True.

True
inner_fill_chars bool

whether to include inner fill characters. Defaults to False.

False
outer_fill_chars bool

whether to include outer fill characters. Defaults to False.

False
added_chars bool

whether to include added characters. Defaults to False.

False
sort CharacterSort

order to sort the characters. Defaults to CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT.

TOP_TO_BOTTOM_LEFT_TO_RIGHT

Returns:

Type Description
list[EffectCharacter]

list[EffectCharacter]: list of EffectCharacters in the terminal

Source code in terminaltexteffects/engine/terminal.py
def get_characters(
    self,
    *,
    input_chars: bool = True,
    inner_fill_chars: bool = False,
    outer_fill_chars: bool = False,
    added_chars: bool = False,
    sort: CharacterSort = CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT,
) -> list[EffectCharacter]:
    """Get a list of all EffectCharacters in the terminal with an optional sort.

    Args:
        input_chars (bool, optional): whether to include input characters. Defaults to True.
        inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False.
        outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False.
        added_chars (bool, optional): whether to include added characters. Defaults to False.
        sort (CharacterSort, optional): order to sort the characters.
            Defaults to CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT.

    Returns:
        list[EffectCharacter]: list of EffectCharacters in the terminal

    """
    all_characters: list[EffectCharacter] = []
    if input_chars:
        all_characters.extend(self._input_characters)
    if inner_fill_chars:
        all_characters.extend(self._inner_fill_characters)
    if outer_fill_chars:
        all_characters.extend(self._outer_fill_characters)
    if added_chars:
        all_characters.extend(self._added_characters)

    # default sort TOP_TO_BOTTOM_LEFT_TO_RIGHT
    all_characters.sort(key=lambda character: (-character.input_coord.row, character.input_coord.column))

    if sort is self.CharacterSort.RANDOM:
        random.shuffle(all_characters)

    elif sort in (self.CharacterSort.TOP_TO_BOTTOM_LEFT_TO_RIGHT, self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT):
        if sort is self.CharacterSort.BOTTOM_TO_TOP_RIGHT_TO_LEFT:
            all_characters.reverse()

    elif sort in (self.CharacterSort.BOTTOM_TO_TOP_LEFT_TO_RIGHT, self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT):
        all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column))
        if sort is self.CharacterSort.TOP_TO_BOTTOM_RIGHT_TO_LEFT:
            all_characters.reverse()

    elif sort in (self.CharacterSort.OUTSIDE_ROW_TO_MIDDLE, self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE):
        all_characters = [
            all_characters.pop(0) if i % 2 == 0 else all_characters.pop(-1) for i in range(len(all_characters))
        ]
        if sort is self.CharacterSort.MIDDLE_ROW_TO_OUTSIDE:
            all_characters.reverse()
    else:
        raise InvalidCharacterSortError(sort)

    return all_characters

get_characters_grouped(grouping=CharacterGroup.ROW_TOP_TO_BOTTOM, *, input_chars=True, inner_fill_chars=False, outer_fill_chars=False, added_chars=False)

Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping.

Parameters:

Name Type Description Default
grouping CharacterGroup

order to group the characters. Defaults to ROW_TOP_TO_BOTTOM.

ROW_TOP_TO_BOTTOM
input_chars bool

whether to include input characters. Defaults to True.

True
inner_fill_chars bool

whether to include inner fill characters. Defaults to False.

False
outer_fill_chars bool

whether to include outer fill characters. Defaults to False.

False
added_chars bool

whether to include added characters. Defaults to False.

False

Returns:

Type Description
list[list[EffectCharacter]]

list[list[EffectCharacter]]: list of lists of EffectCharacters in the terminal. Inner lists correspond to groups as specified in the grouping.

Source code in terminaltexteffects/engine/terminal.py
def get_characters_grouped(
    self,
    grouping: CharacterGroup = CharacterGroup.ROW_TOP_TO_BOTTOM,
    *,
    input_chars: bool = True,
    inner_fill_chars: bool = False,
    outer_fill_chars: bool = False,
    added_chars: bool = False,
) -> list[list[EffectCharacter]]:
    """Get a list of all EffectCharacters grouped by the specified CharacterGroup grouping.

    Args:
        grouping (CharacterGroup, optional): order to group the characters. Defaults to ROW_TOP_TO_BOTTOM.
        input_chars (bool, optional): whether to include input characters. Defaults to True.
        inner_fill_chars (bool, optional): whether to include inner fill characters. Defaults to False.
        outer_fill_chars (bool, optional): whether to include outer fill characters. Defaults to False.
        added_chars (bool, optional): whether to include added characters. Defaults to False.

    Returns:
        list[list[EffectCharacter]]: list of lists of EffectCharacters in the terminal. Inner lists correspond
            to groups as specified in the grouping.

    """
    all_characters: list[EffectCharacter] = []
    if input_chars:
        all_characters.extend(self._input_characters)
    if inner_fill_chars:
        all_characters.extend(self._inner_fill_characters)
    if outer_fill_chars:
        all_characters.extend(self._outer_fill_characters)
    if added_chars:
        all_characters.extend(self._added_characters)

    all_characters.sort(key=lambda character: (character.input_coord.row, character.input_coord.column))

    if grouping in (self.CharacterGroup.COLUMN_LEFT_TO_RIGHT, self.CharacterGroup.COLUMN_RIGHT_TO_LEFT):
        columns = []
        for column_index in range(self.canvas.right + 1):
            characters_in_column = [
                character for character in all_characters if character.input_coord.column == column_index
            ]
            if characters_in_column:
                columns.append(characters_in_column)
        if grouping == self.CharacterGroup.COLUMN_RIGHT_TO_LEFT:
            columns.reverse()
        return columns

    if grouping in (self.CharacterGroup.ROW_BOTTOM_TO_TOP, self.CharacterGroup.ROW_TOP_TO_BOTTOM):
        rows = []
        for row_index in range(self.canvas.top + 1):
            characters_in_row = [
                character for character in all_characters if character.input_coord.row == row_index
            ]
            if characters_in_row:
                rows.append(characters_in_row)
        if grouping == self.CharacterGroup.ROW_TOP_TO_BOTTOM:
            rows.reverse()
        return rows
    if grouping in (
        self.CharacterGroup.DIAGONAL_BOTTOM_LEFT_TO_TOP_RIGHT,
        self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT,
    ):
        diagonals = []
        for diagonal_index in range(self.canvas.top + self.canvas.right + 1):
            characters_in_diagonal = [
                character
                for character in all_characters
                if character.input_coord.row + character.input_coord.column == diagonal_index
            ]
            if characters_in_diagonal:
                diagonals.append(characters_in_diagonal)
        if grouping == self.CharacterGroup.DIAGONAL_TOP_RIGHT_TO_BOTTOM_LEFT:
            diagonals.reverse()
        return diagonals
    if grouping in (
        self.CharacterGroup.DIAGONAL_TOP_LEFT_TO_BOTTOM_RIGHT,
        self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT,
    ):
        diagonals = []
        for diagonal_index in range(self.canvas.left - self.canvas.top, self.canvas.right - self.canvas.bottom + 1):
            characters_in_diagonal = [
                character
                for character in all_characters
                if character.input_coord.column - character.input_coord.row == diagonal_index
            ]
            if characters_in_diagonal:
                diagonals.append(characters_in_diagonal)
        if grouping == self.CharacterGroup.DIAGONAL_BOTTOM_RIGHT_TO_TOP_LEFT:
            diagonals.reverse()
        return diagonals
    if grouping in (
        self.CharacterGroup.CENTER_TO_OUTSIDE_DIAMONDS,
        self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
    ):
        distance_map: dict[int, list[EffectCharacter]] = {}
        center_out = []
        for character in all_characters:
            distance = abs(character.input_coord.column - self.canvas.center.column) + abs(
                character.input_coord.row - self.canvas.center.row,
            )
            if distance not in distance_map:
                distance_map[distance] = []
            distance_map[distance].append(character)
        for distance in sorted(
            distance_map.keys(),
            reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
        ):
            center_out = [
                distance_map[distance]
                for distance in sorted(
                    distance_map.keys(),
                    reverse=grouping is self.CharacterGroup.OUTSIDE_TO_CENTER_DIAMONDS,
                )
            ]
        return center_out

    raise InvalidCharacterGroupError(grouping)

get_formatted_output_string()

Get the formatted output string based on the current terminal state.

This method updates the internal terminal representation state before returning the formatted output string.

Returns:

Name Type Description
str str

The formatted output string.

Source code in terminaltexteffects/engine/terminal.py
def get_formatted_output_string(self) -> str:
    """Get the formatted output string based on the current terminal state.

    This method updates the internal terminal representation state before returning the formatted output string.

    Returns:
        str: The formatted output string.

    """
    self._update_terminal_state()
    return "\n".join(self.terminal_state[::-1])

get_input_colors(sort=ColorSort.MOST_TO_LEAST)

Get a list of colors derived from the input text ansi sequences with an optional sort.

Parameters:

Name Type Description Default
sort ColorSort

Sort the colors. Defaults to ColorSort.MOST_TO_LEAST.

MOST_TO_LEAST

Raises:

Type Description
ValueError

If an invalid sort option is provided.

Returns:

Type Description
list[Color]

list[Color]: list of Colors

Source code in terminaltexteffects/engine/terminal.py
def get_input_colors(self, sort: ColorSort = ColorSort.MOST_TO_LEAST) -> list[Color]:
    """Get a list of colors derived from the input text ansi sequences with an optional sort.

    Args:
        sort (ColorSort, optional): Sort the colors. Defaults to ColorSort.MOST_TO_LEAST.

    Raises:
        ValueError: If an invalid sort option is provided.

    Returns:
        list[Color]: list of Colors

    """
    if sort == self.ColorSort.MOST_TO_LEAST:
        return sorted(
            self._input_colors_frequency.keys(),
            key=lambda color: self._input_colors_frequency[color],
            reverse=True,
        )
    if sort == self.ColorSort.RANDOM:
        colors = list(self._input_colors_frequency.keys())
        random.shuffle(colors)
        return colors
    if sort == self.ColorSort.LEAST_TO_MOST:
        return sorted(self._input_colors_frequency.keys(), key=lambda color: self._input_colors_frequency[color])
    raise InvalidColorSortError(sort)

get_piped_input() staticmethod

Get the piped input from stdin.

This method checks if there is any piped input from the standard input (stdin). If there is no piped input, it returns an empty string. If there is piped input, it reads the input data from stdin and returns it as a string.

The sys.stdin.isatty() check is used to determine if the program is being run interactively or if there is piped input. When the program is run interactively, sys.stdin.isatty() returns True, indicating that there is no piped input. In this case, the method returns an empty string.

Returns:

Name Type Description
str str

The piped input from stdin as a string, or an empty string if there is no piped input.

Source code in terminaltexteffects/engine/terminal.py
@staticmethod
def get_piped_input() -> str:
    """Get the piped input from stdin.

    This method checks if there is any piped input from the standard input (stdin).
    If there is no piped input, it returns an empty string.
    If there is piped input, it reads the input data from stdin and returns it as a string.

    The `sys.stdin.isatty()` check is used to determine if the program is being run interactively
    or if there is piped input. When the program is run interactively, `sys.stdin.isatty()` returns True,
    indicating that there is no piped input. In this case, the method returns an empty string.

    Returns:
        str: The piped input from stdin as a string, or an empty string if there is no piped input.

    """
    if sys.stdin.isatty():
        return ""
    return sys.stdin.read()

move_cursor_to_top()

Restores the cursor position to the top of the canvas.

Source code in terminaltexteffects/engine/terminal.py
def move_cursor_to_top(self) -> None:
    """Restores the cursor position to the top of the canvas."""
    sys.stdout.write(ansitools.dec_restore_cursor_position())
    sys.stdout.write(ansitools.dec_save_cursor_position())
    sys.stdout.write(ansitools.move_cursor_up(self.visible_top))

prep_canvas()

Prepare the terminal for the effect by adding empty lines and hiding the cursor.

Source code in terminaltexteffects/engine/terminal.py
def prep_canvas(self) -> None:
    """Prepare the terminal for the effect by adding empty lines and hiding the cursor."""
    sys.stdout.write(ansitools.hide_cursor())
    sys.stdout.write("\n" * (self.visible_top))
    sys.stdout.write(ansitools.dec_save_cursor_position())

print(output_string)

Print the current terminal state to stdout while preserving the cursor position.

Parameters:

Name Type Description Default
output_string str

The string to be printed.

required
Source code in terminaltexteffects/engine/terminal.py
def print(self, output_string: str) -> None:
    """Print the current terminal state to stdout while preserving the cursor position.

    Args:
        output_string (str): The string to be printed.

    """
    self.move_cursor_to_top()
    sys.stdout.write(output_string)
    sys.stdout.flush()

restore_cursor(end_symbol='\n')

Restores the cursor visibility and prints the end_symbol.

Parameters:

Name Type Description Default
end_symbol str

The symbol to print after the effect has completed. Defaults to newline.

'\n'
Source code in terminaltexteffects/engine/terminal.py
def restore_cursor(self, end_symbol: str = "\n") -> None:
    """Restores the cursor visibility and prints the end_symbol.

    Args:
        end_symbol (str, optional): The symbol to print after the effect has completed. Defaults to newline.

    """
    sys.stdout.write(ansitools.show_cursor())
    sys.stdout.write(end_symbol)

set_character_visibility(character, is_visible)

Set the visibility of a character.

Parameters:

Name Type Description Default
character EffectCharacter

the character to set visibility for

required
is_visible bool

whether the character should be visible

required
Source code in terminaltexteffects/engine/terminal.py
def set_character_visibility(self, character: EffectCharacter, is_visible: bool) -> None:  # noqa: FBT001
    """Set the visibility of a character.

    Args:
        character (EffectCharacter): the character to set visibility for
        is_visible (bool): whether the character should be visible

    """
    character._is_visible = is_visible
    if is_visible:
        self._visible_characters.add(character)
    else:
        self._visible_characters.discard(character)