Created
July 22, 2025 15:15
-
-
Save GeneralD/e8f29722a2c84b677a5530edbd313e68 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| """ | |
| Text Style Converter - Convert plain text to Unicode styled text | |
| Similar to PlainStyle (https://www.shapoco.net/plainstyle/) | |
| """ | |
| import sys | |
| import argparse | |
| from typing import Dict, Callable, Optional, List, Tuple | |
| from dataclasses import dataclass | |
| @dataclass(frozen=True) | |
| class Colors: | |
| """ANSI color codes for terminal output.""" | |
| RED: str = '\033[0;31m' | |
| GREEN: str = '\033[0;32m' | |
| YELLOW: str = '\033[1;33m' | |
| BLUE: str = '\033[0;34m' | |
| PURPLE: str = '\033[0;35m' | |
| CYAN: str = '\033[0;36m' | |
| NC: str = '\033[0m' | |
| @dataclass(frozen=True) | |
| class StyleConfig: | |
| """Configuration for a Unicode style.""" | |
| name: str | |
| bases: Dict[str, int] | |
| special_chars: Optional[Dict] = None | |
| aliases: Optional[List[str]] = None | |
| # Unicode style configurations | |
| STYLE_CONFIGS = [ | |
| StyleConfig('bold', {'upper': 0x1d400, 'lower': 0x1d41a, 'digit': 0x1d7ce}), | |
| StyleConfig('italic', {'upper': 0x1d434, 'lower': 0x1d44e}, | |
| {'h': 0x210E}), | |
| StyleConfig('bolditalic', {'upper': 0x1d468, 'lower': 0x1d482}, | |
| aliases=['bold-italic']), | |
| StyleConfig('script', {'upper': 0x1d49c, 'lower': 0x1d4b6}, { | |
| 'B': 0x212C, 'E': 0x2130, 'F': 0x2131, 'H': 0x210B, | |
| 'I': 0x2110, 'L': 0x2112, 'M': 0x2133, 'R': 0x211B, | |
| 'e': 0x212F, 'g': 0x210A, 'o': 0x2134 | |
| }), | |
| StyleConfig('boldscript', {'upper': 0x1d4d0, 'lower': 0x1d4ea}, | |
| aliases=['bold-script']), | |
| StyleConfig('sans', {'upper': 0x1d5a0, 'lower': 0x1d5ba, 'digit': 0x1d7e2}, | |
| aliases=['sans-serif']), | |
| StyleConfig('sansbold', {'upper': 0x1d5d4, 'lower': 0x1d5ee, 'digit': 0x1d7ec}, | |
| aliases=['sans-bold']), | |
| StyleConfig('sansitalic', {'upper': 0x1d608, 'lower': 0x1d622}, | |
| aliases=['sans-italic']), | |
| StyleConfig('sansbolditalic', {'upper': 0x1d63c, 'lower': 0x1d656}, | |
| aliases=['sans-bold-italic']), | |
| StyleConfig('monospace', {'upper': 0x1d670, 'lower': 0x1d68a, 'digit': 0x1d7f6}, | |
| aliases=['mono']), | |
| StyleConfig('fraktur', {'upper': 0x1d504, 'lower': 0x1d51e}, { | |
| 'C': 0x212D, 'H': 0x210C, 'I': 0x2111, 'R': 0x211C, 'Z': 0x2128 | |
| }, ['gothic']), | |
| StyleConfig('double', {'upper': 0x1d538, 'lower': 0x1d552, 'digit': 0x1d7d8}, { | |
| 'C': 0x2102, 'H': 0x210D, 'N': 0x2115, 'P': 0x2119, | |
| 'Q': 0x211A, 'R': 0x211D, 'Z': 0x2124 | |
| }, ['doublestruck']), | |
| StyleConfig('circled', {'upper': 0x24b6, 'lower': 0x24d0, 'digit': 0x2460}, | |
| aliases=['circle']), | |
| StyleConfig('blackcircled', {'upper': 0x1f150, 'digit': 0x2776}, | |
| aliases=['black-circled']), | |
| StyleConfig('parenthesized', {'upper': 0x1f110, 'lower': 0x249c, 'digit': 0x2474}, | |
| aliases=['parens']), | |
| StyleConfig('squared', {'upper': 0x1f130}, aliases=['square']), | |
| StyleConfig('blacksquared', {'upper': 0x1f170}, aliases=['black-squared']), | |
| StyleConfig('fullwidth', {'base': 0xff01}, { | |
| ' ': 0x3000, '`': 0x2018, '"': 0x201D | |
| }, ['wide']), | |
| StyleConfig('superscript', {}, { | |
| '0': 0x2070, '1': 0x00b9, '2': 0x00b2, '3': 0x00b3, '4': 0x2074, | |
| '5': 0x2075, '6': 0x2076, '7': 0x2077, '8': 0x2078, '9': 0x2079, | |
| '+': 0x207a, '-': 0x207b, '=': 0x207c, '(': 0x207d, ')': 0x207e, | |
| 'a': 0x1d43, 'b': 0x1d47, 'c': 0x1d9c, 'd': 0x1d48, 'e': 0x1d49, | |
| 'f': 0x1da0, 'g': 0x1d4d, 'h': 0x02b0, 'i': 0x2071, 'j': 0x02b2, | |
| 'k': 0x1d4f, 'l': 0x02e1, 'm': 0x1d50, 'n': 0x207f, 'o': 0x1d52, | |
| 'p': 0x1d56, 'r': 0x02b3, 's': 0x02e2, 't': 0x1d57, 'u': 0x1d58, | |
| 'v': 0x1d5b, 'w': 0x02b7, 'x': 0x02e3, 'y': 0x02b8, 'z': 0x1dbb | |
| }, ['super']), | |
| StyleConfig('subscript', {}, { | |
| '0': 0x2080, '1': 0x2081, '2': 0x2082, '3': 0x2083, '4': 0x2084, | |
| '5': 0x2085, '6': 0x2086, '7': 0x2087, '8': 0x2088, '9': 0x2089, | |
| '+': 0x208a, '-': 0x208b, '=': 0x208c, '(': 0x208d, ')': 0x208e, | |
| 'a': 0x2090, 'e': 0x2091, 'o': 0x2092, 'x': 0x2093, 'h': 0x2095, | |
| 'k': 0x2096, 'l': 0x2097, 'm': 0x2098, 'n': 0x2099, 'p': 0x209a, | |
| 's': 0x209b, 't': 0x209c | |
| }, ['sub']) | |
| ] | |
| # Case conversion styles | |
| CASE_STYLES = { | |
| 'upper': str.upper, 'uppercase': str.upper, 'u': str.upper, | |
| 'lower': str.lower, 'lowercase': str.lower, 'l': str.lower, | |
| 'title': str.title, 'titlecase': str.title, 't': str.title, | |
| } | |
| def convert_char_with_offset(char: str, offset: int) -> str: | |
| """Convert character using Unicode offset.""" | |
| return chr(ord(char) + offset) | |
| def convert_digit_special(char: str, digit_base: int, style_name: str) -> str: | |
| """Handle special digit conversion cases.""" | |
| if style_name == 'circled' and char == '0': | |
| return chr(0x24ea) | |
| elif style_name == 'blackcircled' and char == '0': | |
| return chr(0x24ff) | |
| elif digit_base in [0x2460, 0x2776, 0x2474] and char != '0': | |
| return chr(digit_base + ord(char) - ord('1')) | |
| else: | |
| return chr(digit_base + ord(char) - ord('0')) | |
| def convert_with_config(text: str, config: StyleConfig) -> str: | |
| """Convert text using style configuration.""" | |
| result = [] | |
| for char in text: | |
| converted = False | |
| # Check special characters first | |
| if config.special_chars and char in config.special_chars: | |
| result.append(chr(config.special_chars[char])) | |
| converted = True | |
| elif config.name == 'fullwidth' and '!' <= char <= '~': | |
| result.append(chr(config.bases['base'] + ord(char) - ord('!'))) | |
| converted = True | |
| elif 'upper' in config.bases and 'A' <= char <= 'Z': | |
| result.append(convert_char_with_offset(char, config.bases['upper'] - ord('A'))) | |
| converted = True | |
| elif 'lower' in config.bases and 'a' <= char <= 'z': | |
| result.append(convert_char_with_offset(char, config.bases['lower'] - ord('a'))) | |
| converted = True | |
| elif 'digit' in config.bases and '0' <= char <= '9': | |
| result.append(convert_digit_special(char, config.bases['digit'], config.name)) | |
| converted = True | |
| if not converted: | |
| result.append(char) | |
| return ''.join(result) | |
| def build_style_registry() -> Dict[str, Callable[[str], str]]: | |
| """Build registry of all style conversion functions.""" | |
| registry = {} | |
| # Add Unicode styles | |
| for config in STYLE_CONFIGS: | |
| converter = lambda text, cfg=config: convert_with_config(text, cfg) | |
| registry[config.name] = converter | |
| # Add aliases | |
| if config.aliases: | |
| for alias in config.aliases: | |
| registry[alias] = converter | |
| # Add case conversion styles | |
| registry.update(CASE_STYLES) | |
| return registry | |
| def convert_text(style: str, text: str) -> str: | |
| """Convert text to specified style.""" | |
| registry = build_style_registry() | |
| style_key = style.lower() | |
| if style_key not in registry: | |
| raise ValueError(f"Unknown style '{style}'") | |
| return registry[style_key](text) | |
| def get_style_examples() -> Dict[str, str]: | |
| """Generate examples for all styles.""" | |
| examples = { | |
| 'bold': 'Bold text', 'italic': 'Italic text', 'bolditalic': 'Bold italic', | |
| 'script': 'Script text', 'boldscript': 'Bold script', 'monospace': 'Monospace text', | |
| 'fraktur': 'Fraktur text', 'double': 'Double-struck', 'sans': 'Sans serif', | |
| 'sansbold': 'Sans bold', 'sansitalic': 'Sans italic', 'sansbolditalic': 'Sans bold italic', | |
| 'circled': 'Circled text', 'blackcircled': 'BLACK CIRCLED', 'parenthesized': 'Parenthesized', | |
| 'squared': 'SQUARED', 'blacksquared': 'BLACK SQUARED', 'fullwidth': 'Fullwidth text', | |
| 'superscript': 'superscript text', 'subscript': 'subscript text', | |
| 'upper': 'uppercase text', 'lower': 'LOWERCASE TEXT', 'title': 'title case text' | |
| } | |
| return {style: convert_text(style, text) for style, text in examples.items()} | |
| def create_style_section(styles: Dict[str, str], style_list: List[str], | |
| start_num: int = 1, labels: Optional[List[str]] = None) -> str: | |
| """Create a formatted style section.""" | |
| colors = Colors() | |
| if labels: | |
| return "\n".join([ | |
| f"{colors.GREEN}{label}.{colors.NC} {style:<15} - {styles[style]}" | |
| for label, style in zip(labels, style_list) | |
| ]) | |
| else: | |
| return "\n".join([ | |
| f"{colors.GREEN}{i}.{colors.NC} {style:<15} - {styles[style]}" | |
| for i, style in enumerate(style_list, start_num) | |
| ]) | |
| def show_styles(): | |
| """Display all available text styles with examples.""" | |
| colors = Colors() | |
| styles = get_style_examples() | |
| template = """{cyan}Basic text styles:{nc} | |
| {basic_section} | |
| {cyan}Sans-serif styles:{nc} | |
| {sans_section} | |
| {cyan}Enclosed styles:{nc} | |
| {enclosed_section} | |
| {cyan}Special styles:{nc} | |
| {special_section} | |
| {cyan}Case conversion:{nc} | |
| {case_section}""" | |
| print(template.format( | |
| cyan=colors.CYAN, nc=colors.NC, | |
| basic_section=create_style_section(styles, | |
| ['bold', 'italic', 'bolditalic', 'script', 'boldscript', | |
| 'monospace', 'fraktur', 'double']), | |
| sans_section=create_style_section(styles, | |
| ['sans', 'sansbold', 'sansitalic', 'sansbolditalic'], 9), | |
| enclosed_section=create_style_section(styles, | |
| ['circled', 'blackcircled', 'parenthesized', 'squared', | |
| 'blacksquared', 'fullwidth'], 13), | |
| special_section=create_style_section(styles, | |
| ['superscript', 'subscript'], labels=['sup', 'sub']), | |
| case_section=create_style_section(styles, | |
| ['upper', 'lower', 'title'], labels=['u', 'l', 't']) | |
| )) | |
| def show_usage(): | |
| """Display usage information.""" | |
| colors = Colors() | |
| template = """{yellow}Usage:{nc} | |
| textstyle [style] [text] | |
| textstyle [style] # Read from stdin/pipe | |
| {yellow}Examples:{nc} | |
| textstyle bold "Hello World" | |
| textstyle bold Hello World Multiple Words | |
| textstyle script "Beautiful Text" | |
| echo "Hello World" | textstyle bold | |
| cat file.txt | textstyle italic | |
| """ | |
| print(template.format(yellow=colors.YELLOW, nc=colors.NC)) | |
| show_styles() | |
| def get_input_text(text_args: Optional[str]) -> Optional[str]: | |
| """Get input text from arguments or stdin.""" | |
| if text_args: | |
| return text_args | |
| elif not sys.stdin.isatty(): | |
| return sys.stdin.read().strip() | |
| return None | |
| def run_cli(style: Optional[str], text: Optional[str]) -> int: | |
| """Main CLI logic.""" | |
| colors = Colors() | |
| # Show usage if no arguments and not piped | |
| if not style and not text and sys.stdin.isatty(): | |
| show_usage() | |
| return 0 | |
| input_text = get_input_text(text) | |
| # Validate inputs | |
| if not style: | |
| print(f"{colors.RED}Error: Please provide a style{colors.NC}", file=sys.stderr) | |
| show_usage() | |
| return 1 | |
| if not input_text: | |
| print(f"{colors.RED}Error: Please provide text to convert{colors.NC}", file=sys.stderr) | |
| show_usage() | |
| return 1 | |
| # Convert and output | |
| try: | |
| result = convert_text(style, input_text) | |
| print(result) | |
| return 0 | |
| except ValueError as e: | |
| print(f"{colors.RED}Error: {e}{colors.NC}", file=sys.stderr) | |
| show_usage() | |
| return 1 | |
| def parse_arguments() -> argparse.Namespace: | |
| """Parse command line arguments.""" | |
| parser = argparse.ArgumentParser(description="Convert plain text to Unicode styled text") | |
| parser.add_argument('style', nargs='?', help='Text style to apply') | |
| parser.add_argument('text', nargs='*', help='Text to convert (multiple words joined with spaces)') | |
| parser.add_argument('-l', '--list-styles', action='store_true', help='List all available styles') | |
| return parser.parse_args() | |
| def main() -> int: | |
| """Main entry point.""" | |
| args = parse_arguments() | |
| if args.list_styles: | |
| show_styles() | |
| return 0 | |
| # Join text arguments | |
| text = ' '.join(args.text) if args.text else None | |
| return run_cli(args.style, text) | |
| if __name__ == "__main__": | |
| sys.exit(main()) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment