|
5 | 5 | import glob
|
6 | 6 | import os
|
7 | 7 | import re
|
8 |
| -import shutil |
9 | 8 | import subprocess
|
10 | 9 | import sys
|
11 | 10 | import threading
|
12 | 11 | import unicodedata
|
| 12 | +from enum import Enum |
13 | 13 | from typing import Any, Iterable, List, Optional, TextIO, Union
|
14 | 14 |
|
15 | 15 | from . import constants
|
@@ -363,21 +363,6 @@ def get_exes_in_path(starts_with: str) -> List[str]:
|
363 | 363 | return list(exes_set)
|
364 | 364 |
|
365 | 365 |
|
366 |
| -def center_text(msg: str, *, pad: str = ' ') -> str: |
367 |
| - """Centers text horizontally for display within the current terminal, optionally padding both sides. |
368 |
| -
|
369 |
| - :param msg: message to display in the center |
370 |
| - :param pad: if provided, the first character will be used to pad both sides of the message |
371 |
| - :return: centered message, optionally padded on both sides with pad_char |
372 |
| - """ |
373 |
| - term_width = shutil.get_terminal_size().columns |
374 |
| - surrounded_msg = ' {} '.format(msg) |
375 |
| - if not pad: |
376 |
| - pad = ' ' |
377 |
| - fill_char = pad[:1] |
378 |
| - return surrounded_msg.center(term_width, fill_char) |
379 |
| - |
380 |
| - |
381 | 366 | class StdSim(object):
|
382 | 367 | """
|
383 | 368 | Class to simulate behavior of sys.stdout or sys.stderr.
|
@@ -644,3 +629,151 @@ def basic_complete(text: str, line: str, begidx: int, endidx: int, match_against
|
644 | 629 | :return: a list of possible tab completions
|
645 | 630 | """
|
646 | 631 | return [cur_match for cur_match in match_against if cur_match.startswith(text)]
|
| 632 | + |
| 633 | + |
| 634 | +class TextAlignment(Enum): |
| 635 | + LEFT = 1 |
| 636 | + CENTER = 2 |
| 637 | + RIGHT = 3 |
| 638 | + |
| 639 | + |
| 640 | +def align_text(text: str, alignment: TextAlignment, *, fill_char: str = ' ', |
| 641 | + width: Optional[int] = None, tab_width: int = 4) -> str: |
| 642 | + """ |
| 643 | + Align text for display within a given width. Supports characters with display widths greater than 1. |
| 644 | + ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is |
| 645 | + supported. If text has line breaks, then each line is aligned independently. |
| 646 | +
|
| 647 | + There are convenience wrappers around this function: align_left(), align_center(), and align_right() |
| 648 | +
|
| 649 | + :param text: text to align (can contain multiple lines) |
| 650 | + :param alignment: how to align the text |
| 651 | + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) |
| 652 | + :param width: display width of the aligned text. Defaults to width of the terminal. |
| 653 | + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will |
| 654 | + be converted to a space. |
| 655 | + :return: aligned text |
| 656 | + :raises: TypeError if fill_char is more than one character |
| 657 | + ValueError if text or fill_char contains an unprintable character |
| 658 | + """ |
| 659 | + import io |
| 660 | + import shutil |
| 661 | + |
| 662 | + from . import ansi |
| 663 | + |
| 664 | + # Handle tabs |
| 665 | + text = text.replace('\t', ' ' * tab_width) |
| 666 | + if fill_char == '\t': |
| 667 | + fill_char = ' ' |
| 668 | + |
| 669 | + if len(fill_char) != 1: |
| 670 | + raise TypeError("Fill character must be exactly one character long") |
| 671 | + |
| 672 | + fill_char_width = ansi.ansi_safe_wcswidth(fill_char) |
| 673 | + if fill_char_width == -1: |
| 674 | + raise (ValueError("Fill character is an unprintable character")) |
| 675 | + |
| 676 | + if text: |
| 677 | + lines = text.splitlines() |
| 678 | + else: |
| 679 | + lines = [''] |
| 680 | + |
| 681 | + if width is None: |
| 682 | + width = shutil.get_terminal_size().columns |
| 683 | + |
| 684 | + text_buf = io.StringIO() |
| 685 | + |
| 686 | + for index, line in enumerate(lines): |
| 687 | + if index > 0: |
| 688 | + text_buf.write('\n') |
| 689 | + |
| 690 | + # Use ansi_safe_wcswidth to support characters with display widths |
| 691 | + # greater than 1 as well as ANSI escape sequences |
| 692 | + line_width = ansi.ansi_safe_wcswidth(line) |
| 693 | + if line_width == -1: |
| 694 | + raise(ValueError("Text to align contains an unprintable character")) |
| 695 | + |
| 696 | + # Check if line is wider than the desired final width |
| 697 | + if width <= line_width: |
| 698 | + text_buf.write(line) |
| 699 | + continue |
| 700 | + |
| 701 | + # Calculate how wide each side of filling needs to be |
| 702 | + total_fill_width = width - line_width |
| 703 | + |
| 704 | + if alignment == TextAlignment.LEFT: |
| 705 | + left_fill_width = 0 |
| 706 | + right_fill_width = total_fill_width |
| 707 | + elif alignment == TextAlignment.CENTER: |
| 708 | + left_fill_width = total_fill_width // 2 |
| 709 | + right_fill_width = total_fill_width - left_fill_width |
| 710 | + else: |
| 711 | + left_fill_width = total_fill_width |
| 712 | + right_fill_width = 0 |
| 713 | + |
| 714 | + # Determine how many fill characters are needed to cover the width |
| 715 | + left_fill = (left_fill_width // fill_char_width) * fill_char |
| 716 | + right_fill = (right_fill_width // fill_char_width) * fill_char |
| 717 | + |
| 718 | + # In cases where the fill character display width didn't divide evenly into |
| 719 | + # the gaps being filled, pad the remainder with spaces. |
| 720 | + left_fill += ' ' * (left_fill_width - ansi.ansi_safe_wcswidth(left_fill)) |
| 721 | + right_fill += ' ' * (right_fill_width - ansi.ansi_safe_wcswidth(right_fill)) |
| 722 | + |
| 723 | + text_buf.write(left_fill + line + right_fill) |
| 724 | + |
| 725 | + return text_buf.getvalue() |
| 726 | + |
| 727 | + |
| 728 | +def align_left(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: |
| 729 | + """ |
| 730 | + Left align text for display within a given width. Supports characters with display widths greater than 1. |
| 731 | + ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is |
| 732 | + supported. If text has line breaks, then each line is aligned independently. |
| 733 | +
|
| 734 | + :param text: text to left align (can contain multiple lines) |
| 735 | + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) |
| 736 | + :param width: display width of the aligned text. Defaults to width of the terminal. |
| 737 | + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will |
| 738 | + be converted to a space. |
| 739 | + :return: left-aligned text |
| 740 | + :raises: TypeError if fill_char is more than one character |
| 741 | + ValueError if text or fill_char contains an unprintable character |
| 742 | + """ |
| 743 | + return align_text(text, TextAlignment.LEFT, fill_char=fill_char, width=width, tab_width=tab_width) |
| 744 | + |
| 745 | + |
| 746 | +def align_center(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: |
| 747 | + """ |
| 748 | + Center text for display within a given width. Supports characters with display widths greater than 1. |
| 749 | + ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is |
| 750 | + supported. If text has line breaks, then each line is aligned independently. |
| 751 | +
|
| 752 | + :param text: text to center (can contain multiple lines) |
| 753 | + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) |
| 754 | + :param width: display width of the aligned text. Defaults to width of the terminal. |
| 755 | + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will |
| 756 | + be converted to a space. |
| 757 | + :return: centered text |
| 758 | + :raises: TypeError if fill_char is more than one character |
| 759 | + ValueError if text or fill_char contains an unprintable character |
| 760 | + """ |
| 761 | + return align_text(text, TextAlignment.CENTER, fill_char=fill_char, width=width, tab_width=tab_width) |
| 762 | + |
| 763 | + |
| 764 | +def align_right(text: str, *, fill_char: str = ' ', width: Optional[int] = None, tab_width: int = 4) -> str: |
| 765 | + """ |
| 766 | + Right align text for display within a given width. Supports characters with display widths greater than 1. |
| 767 | + ANSI escape sequences are safely ignored and do not count toward the display width. This means colored text is |
| 768 | + supported. If text has line breaks, then each line is aligned independently. |
| 769 | +
|
| 770 | + :param text: text to right align (can contain multiple lines) |
| 771 | + :param fill_char: character that fills the alignment gap. Defaults to space. (Cannot be a line breaking character) |
| 772 | + :param width: display width of the aligned text. Defaults to width of the terminal. |
| 773 | + :param tab_width: any tabs in the text will be replaced with this many spaces. if fill_char is a tab, then it will |
| 774 | + be converted to a space. |
| 775 | + :return: right-aligned text |
| 776 | + :raises: TypeError if fill_char is more than one character |
| 777 | + ValueError if text or fill_char contains an unprintable character |
| 778 | + """ |
| 779 | + return align_text(text, TextAlignment.RIGHT, fill_char=fill_char, width=width, tab_width=tab_width) |
0 commit comments