diff --git a/toot/commands.py b/toot/commands.py index 18d56a5..bb3edf2 100644 --- a/toot/commands.py +++ b/toot/commands.py @@ -14,12 +14,12 @@ from itertools import chain from textwrap import TextWrapper, wrap from toot import api, config, DEFAULT_INSTANCE, User, App, ConsoleError -from toot.output import green, yellow, print_error +from toot.output import print_out from toot.app import TimelineApp def register_app(instance): - print("Registering application with %s" % green(instance)) + print_out("Registering application with {}".format(instance)) try: response = api.create_app(instance) @@ -30,13 +30,15 @@ def register_app(instance): app = App(instance, base_url, response['client_id'], response['client_secret']) path = config.save_app(app) - print("Application tokens saved to: {}\n".format(green(path))) + print_out("Application tokens saved to: {}\n".format(path)) return app def create_app_interactive(): - instance = input("Choose an instance [%s]: " % green(DEFAULT_INSTANCE)) + print_out("Choose an instance [{}]: ".format(DEFAULT_INSTANCE), end="") + + instance = input() if not instance: instance = DEFAULT_INSTANCE @@ -44,7 +46,8 @@ def create_app_interactive(): def login_interactive(app): - print("\nLog in to " + green(app.instance)) + print_out("\nLog in to {}".format(app.instance)) + email = input('Email: ') password = getpass('Password: ') @@ -52,14 +55,15 @@ def login_interactive(app): raise ConsoleError("Email and password cannot be empty.") try: - print("Authenticating...") + print_out("Authenticating...") response = api.login(app, email, password) except api.ApiError: raise ConsoleError("Login failed") user = User(app.instance, email, response['access_token']) path = config.save_user(user) - print("Access token saved to: " + green(path)) + + print_out("Access token saved to: {}".format(path)) return user @@ -67,7 +71,7 @@ def login_interactive(app): def two_factor_login_interactive(app): """Hacky implementation of two factor authentication""" - print("Log in to " + green(app.instance)) + print_out("Log in to {}".format(app.instance)) email = input('Email: ') password = getpass('Password: ') @@ -114,7 +118,7 @@ def two_factor_login_interactive(app): user = User(app.instance, email, access_token) path = config.save_user(user) - print("Access token saved to: " + green(path)) + print_out("Access token saved to: {}".format(path)) def _print_timeline(item): @@ -137,7 +141,7 @@ def _print_timeline(item): return zip_longest(left_column, right_column, fillvalue="") for left, right in timeline_rows(item): - print("{:30} │ {}".format(left, right)) + print_out("{:30} │ {}".format(left, right)) def _parse_timeline(item): @@ -161,10 +165,10 @@ def timeline(app, user, args): items = api.timeline_home(app, user) parsed_items = [_parse_timeline(t) for t in items] - print("─" * 31 + "┬" + "─" * 88) + print_out("─" * 31 + "┬" + "─" * 88) for item in parsed_items: _print_timeline(item) - print("─" * 31 + "┼" + "─" * 88) + print_out("─" * 31 + "┼" + "─" * 88) def curses(app, user, args): @@ -181,76 +185,76 @@ def post(app, user, args): response = api.post_status(app, user, args.text, media_ids=media_ids, visibility=args.visibility) - print("Toot posted: " + green(response.get('url'))) + print_out("Toot posted: {}".format(response.get('url'))) def auth(app, user, args): if app and user: - print("You are logged in to {} as {}\n".format( - yellow(app.instance), - yellow(user.username) - )) - print("User data: " + green(config.get_user_config_path())) - print("App data: " + green(config.get_instance_config_path(app.instance))) + print_out("You are logged in to {} as {}\n".format( + app.instance, user.username)) + print_out("User data: {}".format(config.get_user_config_path())) + print_out("App data: {}".format(config.get_instance_config_path(app.instance))) else: - print("You are not logged in") + print_out("You are not logged in") def login(app, user, args): app = create_app_interactive() login_interactive(app) - print() - print(green("✓ Successfully logged in.")) + print_out() + print_out("✓ Successfully logged in.") def login_2fa(app, user, args): - print() - print(yellow("Two factor authentication is experimental.")) - print(yellow("If you have problems logging in, please open an issue:")) - print(yellow("https://github.com/ihabunek/toot/issues")) - print() + print_out() + print_out("Two factor authentication is experimental.") + print_out("If you have problems logging in, please open an issue:") + print_out("https://github.com/ihabunek/toot/issues") + print_out() app = create_app_interactive() two_factor_login_interactive(app) - print() - print(green("✓ Successfully logged in.")) + print_out() + print_out("✓ Successfully logged in.") def logout(app, user, args): config.delete_user() - print(green("✓ You are now logged out")) + print_out("✓ You are now logged out.") def upload(app, user, args): response = _do_upload(app, user, args.file) - print("\nSuccessfully uploaded media ID {}, type '{}'".format( - yellow(response['id']), yellow(response['type']))) - print("Original URL: " + green(response['url'])) - print("Preview URL: " + green(response['preview_url'])) - print("Text URL: " + green(response['text_url'])) + print_out() + print_out("Successfully uploaded media ID {}, type '{}'".format( + response['id'], response['type'])) + print_out("Original URL: {}".format(response['url'])) + print_out("Preview URL: {}".format(response['preview_url'])) + print_out("Text URL: {}".format(response['text_url'])) def _print_accounts(accounts): if not accounts: return - print("\nAccounts:") + print_out("\nAccounts:") for account in accounts: - acct = green("@{}".format(account['acct'])) - display_name = account['display_name'] - print("* {} {}".format(acct, display_name)) + print_out("* @{} {}".format( + account['acct'], + account['display_name'] + )) def _print_hashtags(hashtags): if not hashtags: return - print("\nHashtags:") - print(", ".join([green("#" + t) for t in hashtags])) + print_out("\nHashtags:") + print_out(", ".join(["#{}".format(t) for t in hashtags])) def search(app, user, args): @@ -261,7 +265,7 @@ def search(app, user, args): def _do_upload(app, user, file): - print("Uploading media: {}".format(green(file.name))) + print_out("Uploading media: {}".format(file.name)) return api.upload_media(app, user, file) @@ -286,59 +290,59 @@ def _find_account(app, user, account_name): def _print_account(account): - print("{} {}".format(green("@" + account['acct']), account['display_name'])) + print_out("@{} {}".format(account['acct'], account['display_name'])) note = BeautifulSoup(account['note'], "html.parser").get_text() if note: - print("") - print("\n".join(wrap(note))) + print_out("") + print_out("\n".join(wrap(note))) - print("") - print("ID: " + green(account['id'])) - print("Since: " + green(account['created_at'][:19].replace('T', ' @ '))) - print("") - print("Followers: " + yellow(account['followers_count'])) - print("Following: " + yellow(account['following_count'])) - print("Statuses: " + yellow(account['statuses_count'])) - print("") - print(account['url']) + print_out("") + print_out("ID: {}".format(account['id'])) + print_out("Since: {}".format(account['created_at'][:19].replace('T', ' @ '))) + print_out("") + print_out("Followers: {}".format(account['followers_count'])) + print_out("Following: {}".format(account['following_count'])) + print_out("Statuses: {}".format(account['statuses_count'])) + print_out("") + print_out(account['url']) def follow(app, user, args): account = _find_account(app, user, args.account) api.follow(app, user, account['id']) - print(green("✓ You are now following %s" % args.account)) + print_out("✓ You are now following {}".format(args.account)) def unfollow(app, user, args): account = _find_account(app, user, args.account) api.unfollow(app, user, account['id']) - print(green("✓ You are no longer following %s" % args.account)) + print_out("✓ You are no longer following {}".format(args.account)) def mute(app, user, args): account = _find_account(app, user, args.account) api.mute(app, user, account['id']) - print(green("✓ You have muted %s" % args.account)) + print_out("✓ You have muted {}".format(args.account)) def unmute(app, user, args): account = _find_account(app, user, args.account) api.unmute(app, user, account['id']) - print(green("✓ %s is no longer muted" % args.account)) + print_out("✓ {} is no longer muted".format(args.account)) def block(app, user, args): account = _find_account(app, user, args.account) api.block(app, user, account['id']) - print(green("✓ You are now blocking %s" % args.account)) + print_out("✓ You are now blocking {}".format(args.account)) def unblock(app, user, args): account = _find_account(app, user, args.account) api.unblock(app, user, account['id']) - print(green("✓ %s is no longer blocked" % args.account)) + print_out("✓ {} is no longer blocked".format(args.account)) def whoami(app, user, args): diff --git a/toot/console.py b/toot/console.py index b734228..c8d6344 100644 --- a/toot/console.py +++ b/toot/console.py @@ -9,7 +9,7 @@ import logging from argparse import ArgumentParser, FileType from collections import namedtuple from toot import config, api, commands, ConsoleError, CLIENT_NAME, CLIENT_WEBSITE -from toot.output import print_error +from toot.output import print_out, print_err VISIBILITY_CHOICES = ['public', 'unlisted', 'private', 'direct'] @@ -26,7 +26,14 @@ def visibility(value): Command = namedtuple("Command", ["name", "description", "require_auth", "arguments"]) -# Some common arguments: +common_args = [ + (["--no-color"], { + "help": "don't use ANSI colors in output", + "action": 'store_true', + "default": False, + }) +] + account_arg = (["account"], { "help": "account name, e.g. 'Gargron' or 'polymerwitch@toot.cat'", }) @@ -202,20 +209,21 @@ def print_usage(): ("Accounts", ACCOUNTS_COMMANDS), ] - print(CLIENT_NAME) + print_out("{}".format(CLIENT_NAME)) for name, cmds in groups: - print("") - print(name + ":") + print_out("") + print_out(name + ":") for cmd in cmds: - print(" toot", cmd.name.ljust(max_name_len + 2), cmd.description) + cmd_name = cmd.name.ljust(max_name_len + 2) + print_out(" toot {} {}".format(cmd_name, cmd.description)) - print("") - print("To get help for each command run:") - print(" toot --help") - print("") - print(CLIENT_WEBSITE) + print_out("") + print_out("To get help for each command run:") + print_out(" toot --help") + print_out("") + print_out("{}".format(CLIENT_WEBSITE)) def get_argument_parser(name, command): @@ -224,7 +232,7 @@ def get_argument_parser(name, command): description=command.description, epilog=CLIENT_WEBSITE) - for args, kwargs in command.arguments: + for args, kwargs in command.arguments + common_args: parser.add_argument(*args, **kwargs) return parser @@ -234,7 +242,7 @@ def run_command(app, user, name, args): command = next((c for c in COMMANDS if c.name == name), None) if not command: - print_error("Unknown command '{}'\n".format(name)) + print_err("Unknown command '{}'\n".format(name)) print_usage() return @@ -242,8 +250,8 @@ def run_command(app, user, name, args): parsed_args = parser.parse_args(args) if command.require_auth and (not user or not app): - print_error("This command requires that you are logged in.") - print_error("Please run `toot login` first.") + print_err("This command requires that you are logged in.") + print_err("Please run `toot login` first.") return fn = commands.__dict__.get(name) @@ -276,6 +284,6 @@ def main(): try: run_command(app, user, command_name, args) except ConsoleError as e: - print_error(str(e)) + print_err(str(e)) except api.ApiError as e: - print_error(str(e)) + print_err(str(e)) diff --git a/toot/output.py b/toot/output.py index 2c1da69..0e836d5 100644 --- a/toot/output.py +++ b/toot/output.py @@ -3,35 +3,53 @@ from __future__ import unicode_literals from __future__ import print_function import sys +import re -def _color(text, color): - return "\033[3{}m{}\033[0m".format(color, text) +START_CODES = { + 'red': '\033[31m', + 'green': '\033[32m', + 'yellow': '\033[33m', + 'blue': '\033[34m', + 'magenta': '\033[35m', + 'cyan': '\033[36m', +} + +END_CODE = '\033[0m' + +START_PATTERN = "<(" + "|".join(START_CODES.keys()) + ")>" + +END_PATTERN = "" -def red(text): - return _color(text, 1) +def start_code(match): + name = match.group(1) + return START_CODES[name] -def green(text): - return _color(text, 2) +def colorize(text): + text = re.sub(START_PATTERN, start_code, text) + text = re.sub(END_PATTERN, END_CODE, text) + + return text -def yellow(text): - return _color(text, 3) +def strip_tags(text): + text = re.sub(START_PATTERN, '', text) + text = re.sub(END_PATTERN, '', text) + + return text -def blue(text): - return _color(text, 4) +USE_ANSI_COLOR = "--no-color" not in sys.argv -def magenta(text): - return _color(text, 5) +def print_out(*args, **kwargs): + args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args] + print(*args, **kwargs) -def cyan(text): - return _color(text, 6) - - -def print_error(text): - print(red(text), file=sys.stderr) +def print_err(*args, **kwargs): + args = ["{}".format(a) for a in args] + args = [colorize(a) if USE_ANSI_COLOR else strip_tags(a) for a in args] + print(*args, file=sys.stderr, **kwargs)