1
0
mirror of https://github.com/ihabunek/toot synced 2024-12-23 07:27:12 +01:00
Toot-Mastodon-CLI-TUI-clien.../toot/utils/__init__.py
Ivan Habunek 00baabf7aa
Store temp file when using editor to post
In case of failed posting the status is not lost and the user can
recover it and continue posting.

fixes #311
2023-02-20 19:54:47 +01:00

152 lines
3.6 KiB
Python

import os
import re
import socket
import subprocess
import tempfile
import unicodedata
import warnings
from bs4 import BeautifulSoup
from toot.exceptions import ConsoleError
def str_bool(b):
"""Convert boolean to string, in the way expected by the API."""
return "true" if b else "false"
def get_text(html):
"""Converts html to text, strips all tags."""
# Ignore warnings made by BeautifulSoup, if passed something that looks like
# a file (e.g. a dot which matches current dict), it will warn that the file
# should be opened instead of passing a filename.
with warnings.catch_warnings():
warnings.simplefilter("ignore")
text = BeautifulSoup(html.replace(''', "'"), "html.parser").get_text()
return unicodedata.normalize('NFKC', text)
def parse_html(html):
"""Attempt to convert html to plain text while keeping line breaks.
Returns a list of paragraphs, each being a list of lines.
"""
paragraphs = re.split("</?p[^>]*>", html)
# Convert <br>s to line breaks and remove empty paragraphs
paragraphs = [re.split("<br */?>", p) for p in paragraphs if p]
# Convert each line in each paragraph to plain text:
return [[get_text(line) for line in p] for p in paragraphs]
def format_content(content):
"""Given a Status contents in HTML, converts it into lines of plain text.
Returns a generator yielding lines of content.
"""
paragraphs = parse_html(content)
first = True
for paragraph in paragraphs:
if not first:
yield ""
for line in paragraph:
yield line
first = False
def domain_exists(name):
try:
socket.gethostbyname(name)
return True
except OSError:
return False
def assert_domain_exists(domain):
if not domain_exists(domain):
raise ConsoleError("Domain {} not found".format(domain))
EOF_KEY = "Ctrl-Z" if os.name == 'nt' else "Ctrl-D"
def multiline_input():
"""Lets user input multiple lines of text, terminated by EOF."""
lines = []
while True:
try:
lines.append(input())
except EOFError:
break
return "\n".join(lines).strip()
EDITOR_DIVIDER = "------------------------ >8 ------------------------"
EDITOR_INPUT_INSTRUCTIONS = f"""
{EDITOR_DIVIDER}
Do not modify or remove the line above.
Enter your toot above it.
Everything below it will be ignored.
"""
def editor_input(editor: str, initial_text: str):
"""Lets user input text using an editor."""
tmp_path = _tmp_status_path()
initial_text = (initial_text or "") + EDITOR_INPUT_INSTRUCTIONS
if not _use_existing_tmp_file(tmp_path):
with open(tmp_path, "w") as f:
f.write(initial_text)
f.flush()
subprocess.run([editor, tmp_path])
with open(tmp_path) as f:
return f.read().split(EDITOR_DIVIDER)[0].strip()
def read_char(values, default):
values = [v.lower() for v in values]
while True:
value = input().lower()
if value == "":
return default
if value in values:
return value
def delete_tmp_status_file():
try:
os.unlink(_tmp_status_path())
except FileNotFoundError:
pass
def _tmp_status_path() -> str:
tmp_dir = tempfile.gettempdir()
return f"{tmp_dir}/.status.toot"
def _use_existing_tmp_file(tmp_path) -> bool:
from toot.output import print_out
if os.path.exists(tmp_path):
print_out(f"<cyan>Found a draft status at: {tmp_path}</cyan>")
print_out("<cyan>[O]pen (default) or [D]elete?</cyan> ", end="")
char = read_char(["o", "d"], "o")
return char == "o"
return False