Implement composing tweets using an editor

fixes #90
This commit is contained in:
Ivan Habunek 2019-08-22 17:10:37 +02:00
parent 840b2fd476
commit d21cad892c
No known key found for this signature in database
GPG Key ID: CDBD63C43A30BB95
3 changed files with 66 additions and 11 deletions

View File

@ -1,11 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import sys
from toot import api, config from toot import api, config
from toot.auth import login_interactive, login_browser_interactive, create_app_interactive from toot.auth import login_interactive, login_browser_interactive, create_app_interactive
from toot.exceptions import ConsoleError, NotFoundError from toot.exceptions import ConsoleError, NotFoundError
from toot.output import (print_out, print_instance, print_account, from toot.output import (print_out, print_instance, print_account,
print_search_results, print_timeline, print_notifications) print_search_results, print_timeline, print_notifications)
from toot.utils import assert_domain_exists, multiline_input, EOF_KEY from toot.utils import assert_domain_exists, editor_input, multiline_input, EOF_KEY
def get_timeline_generator(app, user, args): def get_timeline_generator(app, user, args):
@ -76,9 +78,17 @@ def curses(app, user, args):
def post(app, user, args): def post(app, user, args):
# TODO: this might be achievable, explore options
if args.editor and not sys.stdin.isatty():
raise ConsoleError("Cannot run editor if not in tty.")
if args.media and len(args.media) > 4: if args.media and len(args.media) > 4:
raise ConsoleError("Cannot attach more than 4 files.") raise ConsoleError("Cannot attach more than 4 files.")
# Read any text that might be piped to stdin
if not args.text and not sys.stdin.isatty():
args.text = sys.stdin.read().rstrip()
if args.media: if args.media:
media = [_do_upload(app, user, file) for file in args.media] media = [_do_upload(app, user, file) for file in args.media]
media_ids = [m["id"] for m in media] media_ids = [m["id"] for m in media]
@ -89,7 +99,9 @@ def post(app, user, args):
if media and not args.text: if media and not args.text:
args.text = "\n".join(m['text_url'] for m in media) args.text = "\n".join(m['text_url'] for m in media)
if not args.text: if args.editor:
args.text = editor_input(args.editor, args.text)
elif not args.text:
print_out("Write or paste your toot. Press <yellow>{}</yellow> to post it.".format(EOF_KEY)) print_out("Write or paste your toot. Press <yellow>{}</yellow> to post it.".format(EOF_KEY))
args.text = multiline_input() args.text = multiline_input()

View File

@ -1,8 +1,9 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import os
import sys
import logging import logging
import os
import shutil
import sys
from argparse import ArgumentParser, FileType, ArgumentTypeError from argparse import ArgumentParser, FileType, ArgumentTypeError
from collections import namedtuple from collections import namedtuple
@ -39,6 +40,21 @@ def timeline_count(value):
return n return n
def editor(value):
if not value:
raise ArgumentTypeError(
"Editor not specified in --editor option and $EDITOR environment "
"variable not set."
)
# Check editor executable exists
exe = shutil.which(value)
if not exe:
raise ArgumentTypeError(f"Editor `{value}` not found")
return exe
Command = namedtuple("Command", ["name", "description", "require_auth", "arguments"]) Command = namedtuple("Command", ["name", "description", "require_auth", "arguments"])
@ -298,6 +314,13 @@ POST_COMMANDS = [
"type": language, "type": language,
"help": "ISO 639-2 language code of the toot, to skip automatic detection", "help": "ISO 639-2 language code of the toot, to skip automatic detection",
}), }),
(["-e", "--editor"], {
"type": editor,
"nargs": "?",
"const": os.getenv("EDITOR", ""), # option given without value
"help": "Specify an editor to compose your toot, "
"defaults to editor defined in $EDITOR env variable.",
}),
], ],
require_auth=True, require_auth=True,
), ),
@ -500,12 +523,6 @@ def main():
filename = os.getenv("TOOT_LOG_FILE") filename = os.getenv("TOOT_LOG_FILE")
logging.basicConfig(level=logging.DEBUG, filename=filename) logging.basicConfig(level=logging.DEBUG, filename=filename)
# If something is piped in, append it to commandline arguments
if not sys.stdin.isatty():
stdin = sys.stdin.read()
if stdin:
sys.argv.append(stdin)
command_name = sys.argv[1] if len(sys.argv) > 1 else None command_name = sys.argv[1] if len(sys.argv) > 1 else None
args = sys.argv[2:] args = sys.argv[2:]
@ -519,5 +536,5 @@ def main():
except (ConsoleError, ApiError) as e: except (ConsoleError, ApiError) as e:
print_err(str(e)) print_err(str(e))
sys.exit(1) sys.exit(1)
except KeyboardInterrupt as e: except KeyboardInterrupt:
pass pass

View File

@ -3,6 +3,8 @@
import os import os
import re import re
import socket import socket
import subprocess
import tempfile
import unicodedata import unicodedata
import warnings import warnings
@ -88,3 +90,27 @@ def multiline_input():
break break
return "\n".join(lines).strip() return "\n".join(lines).strip()
EDITOR_INPUT_INSTRUCTIONS = """
# Please enter your toot. Lines starting with '#' will be ignored, and an empty
# message aborts the post.
"""
def editor_input(editor, initial_text):
"""Lets user input text using an editor."""
initial_text = (initial_text or "") + EDITOR_INPUT_INSTRUCTIONS
with tempfile.NamedTemporaryFile() as f:
f.write(initial_text.encode())
f.flush()
subprocess.run([editor, f.name])
f.seek(0)
text = f.read().decode()
lines = text.strip().splitlines()
lines = (l for l in lines if not l.startswith("#"))
return "\n".join(lines)